diff --git a/.gitignore b/.gitignore index a6ba0805..01b87901 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .*.sw? *~ .idea +.vscode # Test files godog.test diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 86071fe3..de63ac0b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,17 +18,22 @@ --- image: gitlab.protontech.ch:4567/go/bridge-internal:go18 +variables: + GOPRIVATE: gitlab.protontech.ch + before_script: - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null - mkdir -p .cache/bin - export PATH=$(pwd)/.cache/bin:$PATH - export GOPATH="$CI_PROJECT_DIR/.cache" + - git config --global --unset-all url.git@gitlab.protontech.ch:.insteadOf + - git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}/ - make install-dev-dependencies - git checkout . cache: - key: go18-mod + key: go18-gluon paths: - .cache policy: pull @@ -76,7 +81,7 @@ cache-push: script: - echo "" cache: - key: go18-mod + key: go18-gluon paths: - .cache diff --git a/Makefile b/Makefile index 0d80bdf9..592094da 100644 --- a/Makefile +++ b/Makefile @@ -234,12 +234,8 @@ integration-test-bridge: ${MAKE} -C test test-bridge mocks: - mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/users Locator,PanicHandler,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go - mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/listener Listener > internal/users/mocks/listener_mocks.go - mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/store PanicHandler,BridgeUser,ChangeNotifier,Storer > internal/store/mocks/mocks.go - mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/listener Listener > internal/store/mocks/utils_mocks.go - mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/pmapi Client,Manager > pkg/pmapi/mocks/mocks.go - mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/message Fetcher > pkg/message/mocks/mocks.go + mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/bridge TLSReporter,ProxyDialer,Autostarter > internal/bridge/mocks/mocks.go + mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go lint: gofiles lint-golang lint-license lint-dependencies lint-changelog diff --git a/cmd/Desktop-Bridge/main.go b/cmd/Desktop-Bridge/main.go index 45dd746e..68cd45b4 100644 --- a/cmd/Desktop-Bridge/main.go +++ b/cmd/Desktop-Bridge/main.go @@ -17,6 +17,13 @@ package main +import ( + "os" + + "github.com/ProtonMail/proton-bridge/v2/internal/app" + "github.com/sirupsen/logrus" +) + /* ___....___ ^^ __..-:'':__:..:__:'':-..__ @@ -34,41 +41,8 @@ package main ~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^ */ -import ( - "os" - - "github.com/ProtonMail/proton-bridge/v2/internal/app/base" - "github.com/ProtonMail/proton-bridge/v2/internal/app/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/constants" - "github.com/sirupsen/logrus" -) - -const ( - appUsage = "Proton Mail IMAP and SMTP Bridge" - configName = "bridge" - updateURLName = "bridge" - keychainName = "bridge" - cacheVersion = "c11" -) - func main() { - base, err := base.New( - constants.FullAppName, - appUsage, - configName, - updateURLName, - keychainName, - cacheVersion, - ) - if err != nil { - logrus.WithError(err).Fatal("Failed to create app base") - } - // Other instance already running. - if base == nil { - return - } - - if err := bridge.New(base).Run(os.Args); err != nil { - logrus.WithError(err).Fatal("Bridge exited with error") + if err := app.New().Run(os.Args); err != nil { + logrus.Fatal(err) } } diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index 9ea840f5..462682ae 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -26,13 +26,13 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/config/useragent" "github.com/ProtonMail/proton-bridge/v2/internal/constants" "github.com/ProtonMail/proton-bridge/v2/internal/crash" "github.com/ProtonMail/proton-bridge/v2/internal/locations" "github.com/ProtonMail/proton-bridge/v2/internal/logging" "github.com/ProtonMail/proton-bridge/v2/internal/sentry" "github.com/ProtonMail/proton-bridge/v2/internal/updater" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" "github.com/ProtonMail/proton-bridge/v2/internal/versioner" "github.com/bradenaw/juniper/xslices" "github.com/elastic/go-sysinfo" @@ -43,10 +43,9 @@ import ( ) const ( - appName = "Proton Mail Launcher" - configName = "bridge" - exeName = "bridge" - guiName = "bridge-gui" + appName = "Proton Mail Launcher" + exeName = "bridge" + guiName = "bridge-gui" FlagCLI = "--cli" FlagCLIShort = "-c" @@ -62,12 +61,12 @@ func main() { //nolint:funlen crashHandler := crash.NewHandler(reporter.ReportException) defer crashHandler.HandlePanic() - locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName)) + locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName)) if err != nil { l.WithError(err).Fatal("Failed to get locations provider") } - locations := locations.New(locationsProvider, configName) + locations := locations.New(locationsProvider, constants.ConfigName) logsPath, err := locations.ProvideLogsPath() if err != nil { @@ -75,12 +74,10 @@ func main() { //nolint:funlen } crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath)) - if err := logging.Init(logsPath); err != nil { - l.WithError(err).Fatal("Failed to setup logging") + if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil { + logrus.WithError(err).Fatal("Failed to setup logging") } - logging.SetLevel(os.Getenv("VERBOSITY")) - updatesPath, err := locations.ProvideUpdatesPath() if err != nil { l.WithError(err).Fatal("Failed to get updates path") @@ -137,6 +134,7 @@ func main() { //nolint:funlen cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + cmd.Env = os.Environ() // On windows, if you use Run(), a terminal stays open; we don't want that. if //goland:noinspection GoBoolExpressions diff --git a/go.mod b/go.mod index 529a47b8..ae45caf7 100644 --- a/go.mod +++ b/go.mod @@ -5,30 +5,22 @@ 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.20220922143913-ef3617264557 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a - github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 - github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/ProtonMail/go-rfc5322 v0.11.0 - github.com/ProtonMail/go-srp v0.0.5 - github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 github.com/ProtonMail/gopenpgp/v2 v2.4.10 github.com/PuerkitoBio/goquery v1.8.0 github.com/abiosoft/ishell v2.0.0+incompatible - github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 github.com/bradenaw/juniper v0.8.0 github.com/cucumber/godog v0.12.5 github.com/cucumber/messages-go/v16 v16.0.1 - github.com/docker/docker-credential-helpers v0.6.4 + github.com/docker/docker-credential-helpers v0.6.3 github.com/elastic/go-sysinfo v1.8.1 - github.com/emersion/go-imap v1.2.1 - github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4 - github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 - github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c - github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69 + github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317 + github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/emersion/go-message v0.16.0 github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead github.com/emersion/go-smtp v0.15.0 - github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 github.com/go-resty/resty/v2 v2.7.0 @@ -38,17 +30,14 @@ require ( github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba - github.com/keybase/go-keychain v0.0.0-20220610143837-c2ce06069005 - github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/keybase/go-keychain v0.0.0 github.com/miekg/dns v1.1.50 - github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 github.com/pkg/errors v0.9.1 - github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 + github.com/pkg/profile v1.6.0 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 github.com/urfave/cli/v2 v2.16.3 - github.com/vmihailenco/msgpack/v5 v5.3.5 - go.etcd.io/bbolt v1.3.6 + gitlab.protontech.ch/go/liteapi v0.30.0 golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/net v0.1.0 golang.org/x/sys v0.1.0 @@ -59,11 +48,19 @@ require ( ) require ( + ariga.io/atlas v0.7.0 // indirect + entgo.io/ent v0.11.2 // indirect github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect + github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 // indirect github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f // indirect + github.com/ProtonMail/go-srp v0.0.5 // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect + github.com/agext/levenshtein v1.2.3 // indirect 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 @@ -71,32 +68,61 @@ 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 github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.8.1 // indirect + github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.0 // indirect + github.com/goccy/go-json v0.9.11 // 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 github.com/hashicorp/golang-lru v0.5.4 // indirect + 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 github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + 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/tools v0.1.12 // indirect + golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect + golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 84f8af60..7ac615b5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k= +ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -11,18 +13,24 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +entgo.io/ent v0.11.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto= +entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU= github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs= github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/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.20220922143913-ef3617264557 h1:uyiHq7jDgn1p2TeMKRPnVCVs2bHoNL9AYs26UzLYr4I= +github.com/ProtonMail/gluon v0.11.1-0.20220922143913-ef3617264557/go.mod h1:9k3URQEASX9XSA+JEcukjIiK3S6aR9GzhLhwccy8AnI= 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= @@ -31,8 +39,6 @@ github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErI github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs= github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= -github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw= -github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0= github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= @@ -42,8 +48,6 @@ github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwj github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw= github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg= github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs= -github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= -github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= github.com/ProtonMail/gopenpgp/v2 v2.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI= github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc= github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= @@ -52,16 +56,19 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA= -github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +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= @@ -72,7 +79,12 @@ 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= @@ -81,14 +93,20 @@ 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= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo= github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE= @@ -106,20 +124,22 @@ 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= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= -github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4 h1:U6LL6F1dYqXpVTwEbXhcfU8hgpNvmjB9xeOAiHN695o= -github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ= -github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 h1:HGBfonz0q/zq7y3ew+4oy4emHSvk6bkmV0mdDG3E77M= -github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= -github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc= -github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0= -github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69 h1:ltTnRlPdSMMb0a/pg7S31T3g+syYeSS5UVJtiR7ez1Y= -github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM= +github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0= +github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs= github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= @@ -130,6 +150,10 @@ 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= @@ -139,14 +163,31 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo= github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= +github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -154,8 +195,16 @@ 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= @@ -164,20 +213,40 @@ 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= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 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= @@ -218,6 +287,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc= +github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= @@ -230,22 +301,32 @@ github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8 github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 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= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= -github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +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= @@ -259,6 +340,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= @@ -267,25 +350,34 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= -github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -293,6 +385,7 @@ 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= @@ -300,18 +393,21 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 h1:d54EL9l+XteliUfUCGsEwwuk65dmmxX85VXF+9T6+50= -github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285/go.mod h1:fxIDly1xtudczrZeOOlfaUvd2OPb2qZAPuWdU2BsBTk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +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= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -323,13 +419,17 @@ 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= @@ -348,32 +448,44 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 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.30.0 h1:ZpYLDC7LH3nn+O0+SgsTx4YNbU2pIj5fu3jcLvTXWbs= +gitlab.protontech.ch/go/liteapi v0.30.0/go.mod h1:ixp1LUOxOYuB1qf172GdV0ZT8fOomKxVFtIMZeSWg+I= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= 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= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= @@ -401,6 +513,8 @@ 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= @@ -418,6 +532,9 @@ 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= @@ -434,14 +551,18 @@ 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-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +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= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -453,13 +574,15 @@ 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-20200923182605-d9f96fdee20d/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= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -474,6 +597,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -488,6 +612,7 @@ 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= @@ -497,10 +622,12 @@ 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.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa h1:uKcci2q7Qtp6nMTC/AAvfNUAldFtJuHWV9/5QWiypts= +golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -523,13 +650,27 @@ 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= @@ -537,6 +678,7 @@ 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= @@ -548,13 +690,17 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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/api/api.go b/internal/api/api.go deleted file mode 100644 index 190f6083..00000000 --- a/internal/api/api.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package api provides HTTP API of the Bridge. -// -// API endpoints: -// - /focus, see focusHandler -package api - -import ( - "fmt" - "net/http" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/ProtonMail/proton-bridge/v2/pkg/ports" - "github.com/sirupsen/logrus" -) - -var log = logrus.WithField("pkg", "api") //nolint:gochecknoglobals - -type apiServer struct { - host string - settings *settings.Settings - eventListener listener.Listener -} - -// NewAPIServer returns prepared API server struct. -func NewAPIServer(settings *settings.Settings, eventListener listener.Listener) *apiServer { //nolint:revive - return &apiServer{ - host: bridge.Host, - settings: settings, - eventListener: eventListener, - } -} - -// Starts the server. -func (api *apiServer) ListenAndServe() { - mux := http.NewServeMux() - mux.HandleFunc("/focus", wrapper(api, focusHandler)) - - addr := api.getAddress() - server := &http.Server{ - Addr: addr, - Handler: mux, - ReadHeaderTimeout: 5 * time.Second, // fix gosec G112 (vulnerability to [Slowloris](https://www.cloudflare.com/en-gb/learning/ddos/ddos-attack-tools/slowloris/) attack). - } - - log.Info("API listening at ", addr) - if err := server.ListenAndServe(); err != nil { - api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error()) - log.Error("API failed: ", err) - } - defer server.Close() //nolint:errcheck -} - -func (api *apiServer) getAddress() string { - port := api.settings.GetInt(settings.APIPortKey) - newPort := ports.FindFreePortFrom(port) - if newPort != port { - api.settings.SetInt(settings.APIPortKey, newPort) - } - return getAPIAddress(api.host, newPort) -} - -func getAPIAddress(host string, port int) string { - return fmt.Sprintf("%s:%d", host, port) -} diff --git a/internal/api/ctx.go b/internal/api/ctx.go deleted file mode 100644 index 421bc695..00000000 --- a/internal/api/ctx.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package api - -import ( - "net/http" - - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" -) - -// httpHandler with Go's Response and Request. -type httpHandler func(http.ResponseWriter, *http.Request) - -// handler with our context. -type handler func(handlerContext) error - -type handlerContext struct { - req *http.Request - resp http.ResponseWriter - eventListener listener.Listener -} - -func wrapper(api *apiServer, callback handler) httpHandler { - return func(w http.ResponseWriter, req *http.Request) { - ctx := handlerContext{ - req: req, - resp: w, - eventListener: api.eventListener, - } - err := callback(ctx) - if err != nil { - log.Error("API callback of ", req.URL, " failed: ", err) - http.Error(w, err.Error(), 500) - } - } -} diff --git a/internal/api/focus.go b/internal/api/focus.go deleted file mode 100644 index 88047f98..00000000 --- a/internal/api/focus.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package api - -import ( - "fmt" - "net/http" - - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/events" -) - -// focusHandler should be called from other instances (attempt to start bridge -// for the second time) to get focus in the currently running instance. -func focusHandler(ctx handlerContext) error { - log.Info("Focus from other instance") - ctx.eventListener.Emit(events.SecondInstanceEvent, "") - fmt.Fprintf(ctx.resp, "OK") - return nil -} - -// CheckOtherInstanceAndFocus is helper for new instances to check if there is -// already a running instance and get it's focus. -func CheckOtherInstanceAndFocus(port int) error { - addr := getAPIAddress(bridge.Host, port) - resp, err := (&http.Client{}).Get("http://" + addr + "/focus") - if err != nil { - return err - } - defer resp.Body.Close() //nolint:errcheck - - if resp.StatusCode != 200 { - log.Error("Focus error: ", resp.StatusCode) - } - return nil -} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 00000000..aa5ed425 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,149 @@ +package app + +import ( + "fmt" + "path/filepath" + + "github.com/ProtonMail/proton-bridge/v2/internal/constants" + "github.com/ProtonMail/proton-bridge/v2/internal/crash" + "github.com/ProtonMail/proton-bridge/v2/internal/focus" + bridgeCLI "github.com/ProtonMail/proton-bridge/v2/internal/frontend/cli" + "github.com/ProtonMail/proton-bridge/v2/internal/frontend/grpc" + "github.com/ProtonMail/proton-bridge/v2/internal/locations" + "github.com/ProtonMail/proton-bridge/v2/internal/sentry" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" + "github.com/ProtonMail/proton-bridge/v2/pkg/restarter" + "github.com/pkg/profile" + "github.com/urfave/cli/v2" +) + +const ( + flagCPUProfile = "cpu-prof" + flagCPUProfileShort = "p" + + flagMemProfile = "mem-prof" + flagMemProfileShort = "m" + + flagLogLevel = "log-level" + flagLogLevelShort = "l" + + flagCLI = "cli" + flagCLIShort = "c" + + flagNoWindow = "no-window" + flagNonInteractive = "non-interactive" +) + +const ( + appUsage = "Proton Mail IMAP and SMTP Bridge" +) + +func New() *cli.App { + app := cli.NewApp() + + app.Name = constants.FullAppName + app.Usage = appUsage + app.Flags = []cli.Flag{ + &cli.BoolFlag{ + Name: flagCPUProfile, + Aliases: []string{flagCPUProfileShort}, + Usage: "Generate CPU profile", + }, + &cli.BoolFlag{ + Name: flagMemProfile, + Aliases: []string{flagMemProfileShort}, + Usage: "Generate memory profile", + }, + &cli.StringFlag{ + Name: flagLogLevel, + Aliases: []string{flagLogLevelShort}, + Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)", + }, + &cli.BoolFlag{ + Name: flagCLI, + Aliases: []string{flagCLIShort}, + Usage: "Use command line interface", + }, + &cli.BoolFlag{ + Name: flagNoWindow, + Usage: "Don't show window after start", + Hidden: true, + }, + } + + app.Action = run + + return app +} + +func run(c *cli.Context) error { + // If there's another instance already running, try to raise it and exit. + if raised := focus.TryRaise(); raised { + return nil + } + + // Start CPU profile if requested. + if c.Bool(flagCPUProfile) { + p := profile.Start(profile.CPUProfile, profile.ProfilePath("cpu.pprof")) + defer p.Stop() + } + + // Start memory profile if requested. + if c.Bool(flagMemProfile) { + p := profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath("mem.pprof")) + defer p.Stop() + } + + // Create the restarter. + restarter := restarter.New() + defer restarter.Restart() + + // Create a user agent that will be used for all requests. + identifier := useragent.New() + + // Create a crash handler that will send crash reports to sentry. + crashHandler := crash.NewHandler( + sentry.NewReporter(constants.FullAppName, constants.Version, identifier).ReportException, + crash.ShowErrorNotification(constants.FullAppName), + func(r interface{}) error { restarter.Set(true, true); return nil }, + ) + defer crashHandler.HandlePanic() + + // Create a locations provider to determine where to store our files. + provider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName)) + if err != nil { + return fmt.Errorf("could not create locations provider: %w", err) + } + + // Create a new locations object that will be used to provide paths to store files. + locations := locations.New(provider, constants.ConfigName) + + // Initialize the logging. + if err := initLogging(c, locations, crashHandler); err != nil { + return fmt.Errorf("could not initialize logging: %w", err) + } + + // Create the bridge. + bridge, err := newBridge(locations, identifier) + if err != nil { + return fmt.Errorf("could not create bridge: %w", err) + } + defer bridge.Close(c.Context) + + // Start the frontend. + switch { + case c.Bool(flagCLI): + return bridgeCLI.New(bridge).Loop() + + case c.Bool(flagNonInteractive): + select {} + + default: + service, err := grpc.NewService(crashHandler, restarter, locations, bridge, !c.Bool(flagNoWindow)) + if err != nil { + return fmt.Errorf("could not create service: %w", err) + } + + return service.Loop() + } +} diff --git a/internal/app/base/args.go b/internal/app/base/args.go deleted file mode 100644 index 21d36689..00000000 --- a/internal/app/base/args.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package base - -import "strings" - -// StripProcessSerialNumber removes additional flag from macOS. -// More info: -// http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951 -func StripProcessSerialNumber(args []string) []string { - res := args[:0] - - for _, arg := range args { - if !strings.Contains(arg, "-psn_") { - res = append(res, arg) - } - } - - return res -} diff --git a/internal/app/base/base.go b/internal/app/base/base.go deleted file mode 100644 index f9e86a79..00000000 --- a/internal/app/base/base.go +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package base implements a common application base currently shared by bridge and IE. -// The base includes the following: -// - access to standard filesystem locations like config, cache, logging dirs -// - an extensible crash handler -// - versioned cache directory -// - persistent settings -// - event listener -// - credentials store -// - pmapi Manager -// -// In addition, the base initialises logging and reacts to command line arguments -// which control the log verbosity and enable cpu/memory profiling. -package base - -import ( - "math/rand" - "os" - "path/filepath" - "runtime" - "runtime/pprof" - "time" - - "github.com/Masterminds/semver/v3" - "github.com/ProtonMail/go-autostart" - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/api" - "github.com/ProtonMail/proton-bridge/v2/internal/config/cache" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/config/tls" - "github.com/ProtonMail/proton-bridge/v2/internal/config/useragent" - "github.com/ProtonMail/proton-bridge/v2/internal/constants" - "github.com/ProtonMail/proton-bridge/v2/internal/cookies" - "github.com/ProtonMail/proton-bridge/v2/internal/crash" - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/locations" - "github.com/ProtonMail/proton-bridge/v2/internal/logging" - "github.com/ProtonMail/proton-bridge/v2/internal/sentry" - "github.com/ProtonMail/proton-bridge/v2/internal/updater" - "github.com/ProtonMail/proton-bridge/v2/internal/users/credentials" - "github.com/ProtonMail/proton-bridge/v2/internal/versioner" - "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" -) - -const ( - flagCPUProfile = "cpu-prof" - flagCPUProfileShort = "p" - flagMemProfile = "mem-prof" - flagMemProfileShort = "m" - flagLogLevel = "log-level" - flagLogLevelShort = "l" - // FlagCLI indicate to start with command line interface. - FlagCLI = "cli" - flagCLIShort = "c" - flagRestart = "restart" - FlagLauncher = "launcher" - FlagNoWindow = "no-window" -) - -type Base struct { - SentryReporter *sentry.Reporter - CrashHandler *crash.Handler - Locations *locations.Locations - Settings *settings.Settings - Lock *os.File - Cache *cache.Cache - Listener listener.Listener - Creds *credentials.Store - CM pmapi.Manager - CookieJar *cookies.Jar - UserAgent *useragent.UserAgent - Updater *updater.Updater - Versioner *versioner.Versioner - TLS *tls.TLS - Autostart *autostart.App - - Name string // the app's name - usage string // the app's usage description - command string // the command used to launch the app (either the exe path or the launcher path) - restart bool // whether the app is currently set to restart - launcher string // launcher to be used if not set in args - mainExecutable string // mainExecutable the main executable process. - - teardown []func() error // actions to perform when app is exiting -} - -func New( //nolint:funlen - appName, - appUsage, - configName, - updateURLName, - keychainName, - cacheVersion string, -) (*Base, error) { - userAgent := useragent.New() - - sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent) - - crashHandler := crash.NewHandler( - sentryReporter.ReportException, - crash.ShowErrorNotification(appName), - ) - defer crashHandler.HandlePanic() - - rand.Seed(time.Now().UnixNano()) - os.Args = StripProcessSerialNumber(os.Args) - - locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName)) - if err != nil { - return nil, err - } - - locations := locations.New(locationsProvider, configName) - - logsPath, err := locations.ProvideLogsPath() - if err != nil { - return nil, err - } - if err := logging.Init(logsPath); err != nil { - return nil, err - } - crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath)) - - if err := migrateFiles(configName); err != nil { - logrus.WithError(err).Warn("Old config files could not be migrated") - } - - if err := locations.Clean(); err != nil { - return nil, err - } - - settingsPath, err := locations.ProvideSettingsPath() - if err != nil { - return nil, err - } - settingsObj := settings.New(settingsPath) - - lock, err := checkSingleInstance(locations.GetLockFile(), settingsObj) - if err != nil { - logrus.WithError(err).Warnf("%v is already running", appName) - return nil, api.CheckOtherInstanceAndFocus(settingsObj.GetInt(settings.APIPortKey)) - } - - if err := migrateRebranding(settingsObj, keychainName); err != nil { - logrus.WithError(err).Warn("Rebranding migration failed") - } - - cachePath, err := locations.ProvideCachePath() - if err != nil { - return nil, err - } - cache, err := cache.New(cachePath, cacheVersion) - if err != nil { - return nil, err - } - if err := cache.RemoveOldVersions(); err != nil { - return nil, err - } - - listener := listener.New() - events.SetupEvents(listener) - - // If we can't load the keychain for whatever reason, - // we signal to frontend and supply a dummy keychain that always returns errors. - kc, err := keychain.NewKeychain(settingsObj, keychainName) - if err != nil { - listener.Emit(events.CredentialsErrorEvent, err.Error()) - kc = keychain.NewMissingKeychain() - } - - cfg := pmapi.NewConfig(configName, constants.Version) - cfg.GetUserAgent = userAgent.String - cfg.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") } - cfg.TLSIssueHandler = func() { listener.Emit(events.TLSCertIssue, "") } - - cm := pmapi.New(cfg) - - sentryReporter.SetClientFromManager(cm) - - cm.AddConnectionObserver(pmapi.NewConnectionObserver( - func() { listener.Emit(events.InternetConnChangedEvent, events.InternetOff) }, - func() { listener.Emit(events.InternetConnChangedEvent, events.InternetOn) }, - )) - - jar, err := cookies.NewCookieJar(settingsObj) - if err != nil { - return nil, err - } - - cm.SetCookieJar(jar) - - key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey) - if err != nil { - return nil, err - } - - kr, err := crypto.NewKeyRing(key) - if err != nil { - return nil, err - } - - updatesDir, err := locations.ProvideUpdatesPath() - if err != nil { - return nil, err - } - - versioner := versioner.New(updatesDir) - installer := updater.NewInstaller(versioner) - updater := updater.New( - cm, - installer, - settingsObj, - kr, - semver.MustParse(constants.Version), - updateURLName, - runtime.GOOS, - ) - - exe, err := os.Executable() - if err != nil { - return nil, err - } - - autostart := &autostart.App{ - Name: startupNameForRebranding(appName), - DisplayName: appName, - Exec: []string{exe, "--" + FlagNoWindow}, - } - - return &Base{ - SentryReporter: sentryReporter, - CrashHandler: crashHandler, - Locations: locations, - Settings: settingsObj, - Lock: lock, - Cache: cache, - Listener: listener, - Creds: credentials.NewStore(kc), - CM: cm, - CookieJar: jar, - UserAgent: userAgent, - Updater: updater, - Versioner: versioner, - TLS: tls.New(settingsPath), - Autostart: autostart, - - Name: appName, - usage: appUsage, - - // By default, the command is the app's executable. - // This can be changed at runtime by using the "--launcher" flag. - command: exe, - // By default, the command is the app's executable. - // This can be changed at runtime by summoning the SetMainExecutable gRPC call. - mainExecutable: exe, - }, nil -} - -func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App { - app := cli.NewApp() - - app.Name = b.Name - app.Usage = b.usage - app.Version = constants.Version - app.Action = b.wrapMainLoop(mainLoop) - app.Flags = []cli.Flag{ - &cli.BoolFlag{ - Name: flagCPUProfile, - Aliases: []string{flagCPUProfileShort}, - Usage: "Generate CPU profile", - }, - &cli.BoolFlag{ - Name: flagMemProfile, - Aliases: []string{flagMemProfileShort}, - Usage: "Generate memory profile", - }, - &cli.StringFlag{ - Name: flagLogLevel, - Aliases: []string{flagLogLevelShort}, - Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)", - }, - &cli.BoolFlag{ - Name: FlagCLI, - Aliases: []string{flagCLIShort}, - Usage: "Use command line interface", - }, - &cli.BoolFlag{ - Name: FlagNoWindow, - Usage: "Don't show window after start", - }, - &cli.StringFlag{ - Name: flagRestart, - Usage: "The number of times the application has already restarted", - Hidden: true, - }, - &cli.StringFlag{ - Name: FlagLauncher, - Usage: "The launcher to use to restart the application", - Hidden: true, - }, - } - - return app -} - -// SetToRestart sets the app to restart the next time it is closed. -func (b *Base) SetToRestart() { - b.restart = true -} - -func (b *Base) ForceLauncher(launcher string) { - b.launcher = launcher - b.setupLauncher(launcher) -} - -func (b *Base) SetMainExecutable(exe string) { - logrus.Info("Main Executable set to ", exe) - b.mainExecutable = exe -} - -// AddTeardownAction adds an action to perform during app teardown. -func (b *Base) AddTeardownAction(fn func() error) { - b.teardown = append(b.teardown, fn) -} - -func (b *Base) wrapMainLoop(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc { //nolint:funlen - return func(c *cli.Context) error { - defer b.CrashHandler.HandlePanic() - defer func() { _ = b.Lock.Close() }() - - // If launcher was used to start the app, use that for restart - // and autostart. - if launcher := c.String(FlagLauncher); launcher != "" { - b.setupLauncher(launcher) - } - - if c.Bool(flagCPUProfile) { - startCPUProfile() - defer pprof.StopCPUProfile() - } - - if c.Bool(flagMemProfile) { - defer makeMemoryProfile() - } - - logging.SetLevel(c.String(flagLogLevel)) - b.CM.SetLogging(logrus.WithField("pkg", "pmapi"), logrus.GetLevel() == logrus.TraceLevel) - - logrus. - WithField("appName", b.Name). - WithField("version", constants.Version). - WithField("revision", constants.Revision). - WithField("build", constants.BuildTime). - WithField("runtime", runtime.GOOS). - WithField("args", os.Args). - Info("Run app") - - b.CrashHandler.AddRecoveryAction(func(interface{}) error { - sentry.Flush(2 * time.Second) - - if c.Int(flagRestart) > maxAllowedRestarts { - logrus. - WithField("restart", c.Int("restart")). - Warn("Not restarting, already restarted too many times") - os.Exit(1) - - return nil - } - - return b.restartApp(true) - }) - - if err := appMainLoop(b, c); err != nil { - return err - } - - if err := b.doTeardown(); err != nil { - return err - } - - if b.restart { - return b.restartApp(false) - } - - return nil - } -} - -func (b *Base) doTeardown() error { - for _, action := range b.teardown { - if err := action(); err != nil { - return err - } - } - - return nil -} - -func (b *Base) setupLauncher(launcher string) { - b.command = launcher - // Bridge supports no-window option which we should use - // for autostart. - b.Autostart.Exec = []string{launcher, "--" + FlagNoWindow} -} diff --git a/internal/app/base/migration.go b/internal/app/base/migration.go deleted file mode 100644 index 6791f967..00000000 --- a/internal/app/base/migration.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package base - -import ( - "os" - "path/filepath" - - "github.com/ProtonMail/proton-bridge/v2/internal/constants" - "github.com/ProtonMail/proton-bridge/v2/internal/locations" - "github.com/sirupsen/logrus" -) - -// migrateFiles migrates files from their old (pre-refactor) locations to their new locations. -// We can remove this eventually. -// -// | entity | old location | new location | -// |-----------|-------------------------------------------|----------------------------------------| -// | prefs | ~/.cache/protonmail//c11/prefs.json | ~/.config/protonmail//prefs.json | -// | c11 1.5.x | ~/.cache/protonmail//c11 | ~/.cache/protonmail//cache/c11 | -// | c11 1.6.x | ~/.cache/protonmail//cache/c11 | ~/.config/protonmail//cache/c11 | -// | updates | ~/.cache/protonmail//updates | ~/.config/protonmail//updates |. -func migrateFiles(configName string) error { - locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName)) - if err != nil { - return err - } - - locations := locations.New(locationsProvider, configName) - userCacheDir := locationsProvider.UserCache() - - if err := migratePrefsFrom15x(locations, userCacheDir); err != nil { - return err - } - if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil { - return err - } - if err := migrateUpdatesFrom16x(configName, locations); err != nil { //nolint:revive It is more clear to structure this way - return err - } - return nil -} - -func migratePrefsFrom15x(locations *locations.Locations, userCacheDir string) error { - newSettingsDir, err := locations.ProvideSettingsPath() - if err != nil { - return err - } - - return moveIfExists( - filepath.Join(userCacheDir, "c11", "prefs.json"), - filepath.Join(newSettingsDir, "prefs.json"), - ) -} - -func migrateCacheFromBoth15xAnd16x(locations *locations.Locations, userCacheDir string) error { - olderCacheDir := userCacheDir - newerCacheDir := locations.GetOldCachePath() - latestCacheDir, err := locations.ProvideCachePath() - if err != nil { - return err - } - - // Migration for versions before 1.6.x. - if err := moveIfExists( - filepath.Join(olderCacheDir, "c11"), - filepath.Join(latestCacheDir, "c11"), - ); err != nil { - return err - } - - // Migration for versions 1.6.x. - return moveIfExists( - filepath.Join(newerCacheDir, "c11"), - filepath.Join(latestCacheDir, "c11"), - ) -} - -func migrateUpdatesFrom16x(configName string, locations *locations.Locations) error { - // In order to properly update Bridge 1.6.X and higher we need to - // change the launcher first. Since this is not part of automatic - // updates the migration must wait until manual update. Until that - // we need to keep old path. - if configName == "bridge" { - return nil - } - - oldUpdatesPath := locations.GetOldUpdatesPath() - // Do not use ProvideUpdatesPath, that creates dir right away. - newUpdatesPath := locations.GetUpdatesPath() - - return moveIfExists(oldUpdatesPath, newUpdatesPath) -} - -func moveIfExists(source, destination string) error { - l := logrus.WithField("source", source).WithField("destination", destination) - - if _, err := os.Stat(source); os.IsNotExist(err) { - l.Info("No need to migrate file, source doesn't exist") - return nil - } - - if _, err := os.Stat(destination); !os.IsNotExist(err) { - // Once migrated, files should not stay in source anymore. Therefore - // if some files are still in source location but target already exist, - // it's suspicious. Could happen by installing new version, then the - // old one because of some reason, and then the new one again. - // Good to see as warning because it could be a reason why Bridge is - // behaving weirdly, like wrong configuration, or db re-sync and so on. - l.Warn("No need to migrate file, target already exists") - return nil - } - - l.Info("Migrating files") - return os.Rename(source, destination) -} diff --git a/internal/app/base/migration_rebranding.go b/internal/app/base/migration_rebranding.go deleted file mode 100644 index 9c7a57fb..00000000 --- a/internal/app/base/migration_rebranding.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package base - -import ( - "errors" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" - "github.com/hashicorp/go-multierror" - "github.com/sirupsen/logrus" -) - -const darwin = "darwin" - -func migrateRebranding(settingsObj *settings.Settings, keychainName string) (result error) { - if err := migrateStartupBeforeRebranding(); err != nil { - result = multierror.Append(result, err) - } - - lastUsedVersion := settingsObj.Get(settings.LastVersionKey) - - // Skipping migration: it is first bridge start or cache was cleared. - if lastUsedVersion == "" { - settingsObj.SetBool(settings.RebrandingMigrationKey, true) - return - } - - // Skipping rest of migration: already done - if settingsObj.GetBool(settings.RebrandingMigrationKey) { - return - } - - switch runtime.GOOS { - case "windows", "linux": - // GODT-1260 we would need admin rights to changes desktop files - // and start menu items. - settingsObj.SetBool(settings.RebrandingMigrationKey, true) - case darwin: - if shouldContinue, err := isMacBeforeRebranding(); !shouldContinue || err != nil { - if err != nil { - result = multierror.Append(result, err) - } - break - } - - if err := migrateMacKeychainBeforeRebranding(settingsObj, keychainName); err != nil { - result = multierror.Append(result, err) - } - - settingsObj.SetBool(settings.RebrandingMigrationKey, true) - } - - return result -} - -// migrateMacKeychainBeforeRebranding deals with write access restriction to -// mac keychain passwords which are caused by application renaming. The old -// passwords are copied under new name in order to have write access afer -// renaming. -func migrateMacKeychainBeforeRebranding(settingsObj *settings.Settings, keychainName string) error { - l := logrus.WithField("pkg", "app/base/migration") - l.Warn("Migrating mac keychain") - - helperConstructor, ok := keychain.Helpers["macos-keychain"] - if !ok { - return errors.New("cannot find macos-keychain helper") - } - - oldKC, err := helperConstructor("ProtonMailBridgeService") - if err != nil { - l.WithError(err).Error("Keychain constructor failed") - return err - } - - idByURL, err := oldKC.List() - if err != nil { - l.WithError(err).Error("List old keychain failed") - return err - } - - newKC, err := keychain.NewKeychain(settingsObj, keychainName) - if err != nil { - return err - } - - for url, id := range idByURL { - li := l.WithField("id", id).WithField("url", url) - userID, secret, err := oldKC.Get(url) - if err != nil { - li.WithField("userID", userID). - WithField("err", err). - Error("Faild to get old item") - continue - } - - if _, _, err := newKC.Get(userID); err == nil { - li.Warn("Skipping migration, item already exists.") - continue - } - - if err := newKC.Put(userID, secret); err != nil { - li.WithError(err).Error("Failed to migrate user") - } - - li.Info("Item migrated") - } - - return nil -} - -// migrateStartupBeforeRebranding removes old startup links. The creation of new links is -// handled by bridge initialisation. -func migrateStartupBeforeRebranding() error { - path, err := os.UserHomeDir() - if err != nil { - return err - } - - switch runtime.GOOS { - case "windows": - path = filepath.Join(path, `AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\ProtonMail Bridge.lnk`) - case "linux": - path = filepath.Join(path, `.config/autostart/ProtonMail Bridge.desktop`) - case darwin: - path = filepath.Join(path, `Library/LaunchAgents/ProtonMail Bridge.plist`) - default: - return errors.New("unknown GOOS") - } - - if _, err := os.Stat(path); os.IsNotExist(err) { - return nil - } - - logrus.WithField("pkg", "app/base/migration").Warn("Migrating autostartup links") - return os.Remove(path) -} - -// startupNameForRebranding returns the name for autostart launcher based on -// type of rebranded instance i.e. update or manual. -// -// This only affects darwin when udpate re-writes the old startup and then -// manual installed it would not run proper exe. Therefore we return "old" name -// for updates and "new" name for manual which would be properly migrated. -// -// For orther (linux and windows) the link is always pointing to launcher which -// path didn't changed. -func startupNameForRebranding(origin string) string { - if runtime.GOOS == darwin { - if path, err := os.Executable(); err == nil && strings.Contains(path, "ProtonMail Bridge") { - return "ProtonMail Bridge" - } - } - - // No need to solve for other OS. See comment above. - return origin -} - -// isBeforeRebranding decide if last used version was older than 2.2.0. If -// cannot decide it returns false with error. -func isMacBeforeRebranding() (bool, error) { - // previous version | update | do mac migration | - // | first | false | - // cleared-cache | manual | false | - // cleared-cache | in-app | false | - // old | in-app | false | - // old in-app | in-app | false | - // old | manual | true | - // old in-app | manual | true | - // manual | in-app | false | - - // Skip if it was in-app update and not manual - if path, err := os.Executable(); err != nil || strings.Contains(path, "ProtonMail Bridge") { - return false, err - } - - return true, nil -} diff --git a/internal/app/base/profiling.go b/internal/app/base/profiling.go deleted file mode 100644 index f5cd4a59..00000000 --- a/internal/app/base/profiling.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package base - -import ( - "os" - "path/filepath" - "runtime" - "runtime/pprof" - - "github.com/sirupsen/logrus" -) - -// startCPUProfile starts CPU pprof. -func startCPUProfile() { - f, err := os.Create("./cpu.pprof") - if err != nil { - logrus.Fatal("Could not create CPU profile: ", err) - } - if err := pprof.StartCPUProfile(f); err != nil { - logrus.Fatal("Could not start CPU profile: ", err) - } -} - -// makeMemoryProfile generates memory pprof. -func makeMemoryProfile() { - name := "./mem.pprof" - f, err := os.Create(name) - if err != nil { - logrus.Fatal("Could not create memory profile: ", err) - } - if abs, err := filepath.Abs(name); err == nil { - name = abs - } - logrus.Info("Writing memory profile to ", name) - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - logrus.Fatal("Could not write memory profile: ", err) - } - _ = f.Close() -} diff --git a/internal/app/base/restart.go b/internal/app/base/restart.go deleted file mode 100644 index b60d9a7a..00000000 --- a/internal/app/base/restart.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package base - -import ( - "os" - "strconv" - - "github.com/sirupsen/logrus" - "golang.org/x/sys/execabs" -) - -// maxAllowedRestarts controls after how many crashes the app will give up restarting. -const maxAllowedRestarts = 10 - -func (b *Base) restartApp(crash bool) error { - var args []string - - if crash { - args = incrementRestartFlag(os.Args)[1:] - defer func() { os.Exit(1) }() - } else { - args = os.Args[1:] - } - - if b.launcher != "" { - args = forceLauncherFlag(args, b.launcher) - } - - args = append(args, "--wait", b.mainExecutable) - - logrus. - WithField("command", b.command). - WithField("args", args). - Warn("Restarting") - - return execabs.Command(b.command, args...).Start() //nolint:gosec -} - -// incrementRestartFlag increments the value of the restart flag. -// If no such flag is present, it is added with initial value 1. -func incrementRestartFlag(args []string) []string { - res := append([]string{}, args...) - - hasFlag := false - - for k, v := range res { - if v != "--restart" { - continue - } - - hasFlag = true - - if k+1 >= len(res) { - continue - } - - n, err := strconv.Atoi(res[k+1]) - if err != nil { - res[k+1] = "1" - } else { - res[k+1] = strconv.Itoa(n + 1) - } - } - - if !hasFlag { - res = append(res, "--restart", "1") - } - - return res -} - -// forceLauncherFlag replace or add the launcher args with the one set in the app. -func forceLauncherFlag(args []string, launcher string) []string { - res := append([]string{}, args...) - - hasFlag := false - - for k, v := range res { - if v != "--launcher" { - continue - } - - if k+1 >= len(res) { - continue - } - - hasFlag = true - res[k+1] = launcher - } - - if !hasFlag { - res = append(res, "--launcher", launcher) - } - - return res -} diff --git a/internal/app/base/restart_test.go b/internal/app/base/restart_test.go deleted file mode 100644 index 9e704f02..00000000 --- a/internal/app/base/restart_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package base - -import ( - "strings" - "testing" - - "github.com/Masterminds/semver/v3" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestIncrementRestartFlag(t *testing.T) { - tests := []struct { - in []string - out []string - }{ - {[]string{"./bridge", "--restart", "1"}, []string{"./bridge", "--restart", "2"}}, - {[]string{"./bridge", "--restart", "2"}, []string{"./bridge", "--restart", "3"}}, - {[]string{"./bridge", "--other", "--restart", "2"}, []string{"./bridge", "--other", "--restart", "3"}}, - {[]string{"./bridge", "--restart", "2", "--other"}, []string{"./bridge", "--restart", "3", "--other"}}, - {[]string{"./bridge", "--restart", "2", "--other", "2"}, []string{"./bridge", "--restart", "3", "--other", "2"}}, - {[]string{"./bridge"}, []string{"./bridge", "--restart", "1"}}, - {[]string{"./bridge", "--something"}, []string{"./bridge", "--something", "--restart", "1"}}, - {[]string{"./bridge", "--something", "--else"}, []string{"./bridge", "--something", "--else", "--restart", "1"}}, - {[]string{"./bridge", "--restart", "bad"}, []string{"./bridge", "--restart", "1"}}, - {[]string{"./bridge", "--restart", "bad", "--other"}, []string{"./bridge", "--restart", "1", "--other"}}, - } - - for _, tt := range tests { - t.Run(strings.Join(tt.in, " "), func(t *testing.T) { - assert.Equal(t, tt.out, incrementRestartFlag(tt.in)) - }) - } -} - -func TestVersionLessThan(t *testing.T) { - r := require.New(t) - - old := semver.MustParse("1.1.0") - current := semver.MustParse("1.1.1") - newer := semver.MustParse("1.1.2") - - r.True(old.LessThan(current)) - r.False(current.LessThan(current)) - r.False(newer.LessThan(current)) -} diff --git a/internal/app/base/singleinstance_unix.go b/internal/app/base/singleinstance_unix.go deleted file mode 100644 index 535db8eb..00000000 --- a/internal/app/base/singleinstance_unix.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build !windows -// +build !windows - -package base - -import ( - "errors" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/Masterminds/semver/v3" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/constants" - "github.com/allan-simon/go-singleinstance" - "golang.org/x/sys/unix" -) - -// checkSingleInstance returns error if a bridge instance is already running -// This instance should be stop and window of running window should be brought -// to focus. -// -// For macOS and Linux when already running version is older than this instance -// it will kill old and continue with this new bridge (i.e. no error returned). -func checkSingleInstance(lockFilePath string, settingsObj *settings.Settings) (*os.File, error) { - if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil { - // Bridge is not runnig, continue normally - return lock, nil - } - - if err := runningVersionIsOlder(settingsObj); err != nil { - return nil, err - } - - pid, err := getPID(lockFilePath) - if err != nil { - return nil, err - } - - if err := unix.Kill(pid, unix.SIGTERM); err != nil { - return nil, err - } - - // Need to wait some time to release file lock - time.Sleep(time.Second) - - return singleinstance.CreateLockFile(lockFilePath) -} - -func getPID(lockFilePath string) (int, error) { - file, err := os.Open(filepath.Clean(lockFilePath)) - if err != nil { - return 0, err - } - defer func() { _ = file.Close() }() - - rawPID := make([]byte, 10) // PID is probably up to 7 digits long, 10 should be enough - n, err := file.Read(rawPID) - if err != nil { - return 0, err - } - - return strconv.Atoi(strings.TrimSpace(string(rawPID[:n]))) -} - -func runningVersionIsOlder(settingsObj *settings.Settings) error { - currentVer, err := semver.StrictNewVersion(constants.Version) - if err != nil { - return err - } - - runningVer, err := semver.StrictNewVersion(settingsObj.Get(settings.LastVersionKey)) - if err != nil { - return err - } - - if !runningVer.LessThan(currentVer) { - return errors.New("running version is not older") - } - - return nil -} diff --git a/internal/app/base/singleinstance_windows.go b/internal/app/base/singleinstance_windows.go deleted file mode 100644 index d0c34752..00000000 --- a/internal/app/base/singleinstance_windows.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build windows -// +build windows - -package base - -import ( - "os" - - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/allan-simon/go-singleinstance" -) - -func checkSingleInstance(lockFilePath string, _ *settings.Settings) (*os.File, error) { - return singleinstance.CreateLockFile(lockFilePath) -} diff --git a/internal/app/bridge.go b/internal/app/bridge.go new file mode 100644 index 00000000..4363f272 --- /dev/null +++ b/internal/app/bridge.go @@ -0,0 +1,205 @@ +package app + +import ( + "encoding/base64" + "fmt" + "os" + "runtime" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/go-autostart" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" + "github.com/ProtonMail/proton-bridge/v2/internal/certs" + "github.com/ProtonMail/proton-bridge/v2/internal/constants" + "github.com/ProtonMail/proton-bridge/v2/internal/dialer" + "github.com/ProtonMail/proton-bridge/v2/internal/locations" + "github.com/ProtonMail/proton-bridge/v2/internal/updater" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" + "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/ProtonMail/proton-bridge/v2/internal/versioner" + "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" + "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" +) + +const vaultSecretName = "bridge-vault-key" + +func newBridge(locations *locations.Locations, identifier *useragent.UserAgent) (*bridge.Bridge, error) { + // Create the underlying dialer used by the bridge. + // It only connects to trusted servers and reports any untrusted servers it finds. + pinningDialer := dialer.NewPinningTLSDialer( + dialer.NewBasicTLSDialer(constants.APIHost), + dialer.NewTLSReporter(constants.APIHost, constants.AppVersion, identifier, dialer.TrustedAPIPins), + dialer.NewTLSPinChecker(dialer.TrustedAPIPins), + ) + + // Create a proxy dialer which switches to a proxy if the request fails. + proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost) + + // Create the autostarter. + autostarter, err := newAutostarter() + if err != nil { + return nil, fmt.Errorf("could not create autostarter: %w", err) + } + + // Create the update installer. + updater, err := newUpdater(locations) + if err != nil { + return nil, fmt.Errorf("could not create updater: %w", err) + } + + // Get the current bridge version. + version, err := semver.NewVersion(constants.Version) + if err != nil { + return nil, fmt.Errorf("could not create version: %w", err) + } + + // Create the encVault. + encVault, insecure, corrupt, err := newVault(locations) + if err != nil { + return nil, fmt.Errorf("could not create vault: %w", err) + } else if insecure { + logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted") + } else if corrupt { + logrus.Warn("The vault is corrupt and has been wiped") + } + + // Install the certificates if needed. + if installed := encVault.GetCertsInstalled(); !installed { + if err := certs.NewInstaller().InstallCert(encVault.GetBridgeTLSCert()); err != nil { + return nil, fmt.Errorf("failed to install certs: %w", err) + } + + if err := encVault.SetCertsInstalled(true); err != nil { + return nil, fmt.Errorf("failed to set certs installed: %w", err) + } + + if err := encVault.SetCertsInstalled(true); err != nil { + return nil, fmt.Errorf("could not set certs installed: %w", err) + } + } + + // Create a new bridge. + bridge, err := bridge.New(constants.APIHost, locations, encVault, identifier, pinningDialer, proxyDialer, autostarter, updater, version) + if err != nil { + return nil, fmt.Errorf("could not create bridge: %w", err) + } + + // If the vault could not be loaded properly, push errors to the bridge. + switch { + case insecure: + bridge.PushError(vault.ErrInsecure) + + case corrupt: + bridge.PushError(vault.ErrCorrupt) + } + + return bridge, nil +} + +func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) { + var insecure bool + + vaultDir, err := locations.ProvideSettingsPath() + if err != nil { + return nil, false, false, fmt.Errorf("could not get vault dir: %w", err) + } + + var vaultKey []byte + + if key, err := getVaultKey(vaultDir); err != nil { + insecure = true + } else { + vaultKey = key + } + + gluonDir, err := locations.ProvideGluonPath() + if err != nil { + return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err) + } + + vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey) + if err != nil { + return nil, false, false, fmt.Errorf("could not create vault: %w", err) + } + + return vault, insecure, corrupt, nil +} + +func getVaultKey(vaultDir string) ([]byte, error) { + helper, err := vault.GetHelper(vaultDir) + if err != nil { + return nil, fmt.Errorf("could not get keychain helper: %w", err) + } + + keychain, err := keychain.NewKeychain(helper, constants.KeyChainName) + if err != nil { + return nil, fmt.Errorf("could not create keychain: %w", err) + } + + secrets, err := keychain.List() + if err != nil { + return nil, fmt.Errorf("could not list keychain: %w", err) + } + + if !slices.Contains(secrets, vaultSecretName) { + tok, err := crypto.RandomToken(32) + if err != nil { + return nil, fmt.Errorf("could not generate random token: %w", err) + } + + if err := keychain.Put(vaultSecretName, base64.StdEncoding.EncodeToString(tok)); err != nil { + return nil, fmt.Errorf("could not put keychain item: %w", err) + } + } + + _, keyEnc, err := keychain.Get(vaultSecretName) + if err != nil { + return nil, fmt.Errorf("could not get keychain item: %w", err) + } + + keyDec, err := base64.StdEncoding.DecodeString(keyEnc) + if err != nil { + return nil, fmt.Errorf("could not decode keychain item: %w", err) + } + + return keyDec, nil +} + +func newAutostarter() (*autostart.App, error) { + exe, err := os.Executable() + if err != nil { + return nil, err + } + + return &autostart.App{ + Name: constants.FullAppName, + DisplayName: constants.FullAppName, + Exec: []string{exe, "--" + flagNoWindow}, + }, nil +} + +func newUpdater(locations *locations.Locations) (*updater.Updater, error) { + updatesDir, err := locations.ProvideUpdatesPath() + if err != nil { + return nil, fmt.Errorf("could not provide updates path: %w", err) + } + + key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey) + if err != nil { + return nil, fmt.Errorf("could not create key from armored: %w", err) + } + + verifier, err := crypto.NewKeyRing(key) + if err != nil { + return nil, fmt.Errorf("could not create key ring: %w", err) + } + + return updater.NewUpdater( + updater.NewInstaller(versioner.New(updatesDir)), + verifier, + constants.UpdateName, + runtime.GOOS, + ), nil +} diff --git a/internal/app/bridge/bridge.go b/internal/app/bridge/bridge.go deleted file mode 100644 index c20f9536..00000000 --- a/internal/app/bridge/bridge.go +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package bridge implements the bridge CLI application. -package bridge - -import ( - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/api" - "github.com/ProtonMail/proton-bridge/v2/internal/app/base" - pkgBridge "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/constants" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" - "github.com/ProtonMail/proton-bridge/v2/internal/imap" - "github.com/ProtonMail/proton-bridge/v2/internal/smtp" - "github.com/ProtonMail/proton-bridge/v2/internal/store" - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" - "github.com/ProtonMail/proton-bridge/v2/internal/updater" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" -) - -const ( - flagLogIMAP = "log-imap" - flagLogSMTP = "log-smtp" - flagNonInteractive = "noninteractive" - - // Memory cache was estimated by empirical usage in past and it was set to 100MB. - // NOTE: This value must not be less than maximal size of one email (~30MB). - inMemoryCacheLimnit = 100 * (1 << 20) -) - -func New(base *base.Base) *cli.App { - app := base.NewApp(main) - - app.Flags = append(app.Flags, []cli.Flag{ - &cli.StringFlag{ - Name: flagLogIMAP, - Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)", - }, - &cli.BoolFlag{ - Name: flagLogSMTP, - Usage: "Enable logging of SMTP communications (may contain decrypted data!)", - }, - &cli.BoolFlag{ - Name: flagNonInteractive, - Usage: "Start Bridge entirely noninteractively", - }, - }...) - - return app -} - -func main(b *base.Base, c *cli.Context) error { //nolint:funlen - cache, cacheErr := loadMessageCache(b) - if cacheErr != nil { - logrus.WithError(cacheErr).Error("Could not load local cache.") - } - - builder := message.NewBuilder( - b.Settings.GetInt(settings.FetchWorkers), - b.Settings.GetInt(settings.AttachmentWorkers), - ) - - bridge := pkgBridge.New( - b.Locations, - b.Cache, - b.Settings, - b.SentryReporter, - b.CrashHandler, - b.Listener, - b.TLS, - b.UserAgent, - cache, - builder, - b.CM, - b.Creds, - b.Updater, - b.Versioner, - b.Autostart, - ) - imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, b.Settings, bridge) - smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge) - - tlsConfig, err := bridge.GetTLSConfig() - if err != nil { - return err - } - - if cacheErr != nil { - bridge.AddError(pkgBridge.ErrLocalCacheUnavailable) - } - - go func() { - defer b.CrashHandler.HandlePanic() - api.NewAPIServer(b.Settings, b.Listener).ListenAndServe() - }() - - go func() { - defer b.CrashHandler.HandlePanic() - imapPort := b.Settings.GetInt(settings.IMAPPortKey) - imap.NewIMAPServer( - b.CrashHandler, - c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all", - c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all", - imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe() - }() - - go func() { - defer b.CrashHandler.HandlePanic() - smtpPort := b.Settings.GetInt(settings.SMTPPortKey) - useSSL := b.Settings.GetBool(settings.SMTPSSLKey) - smtp.NewSMTPServer( - b.CrashHandler, - c.Bool(flagLogSMTP), - smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe() - }() - - // We want to remove old versions if the app exits successfully. - b.AddTeardownAction(b.Versioner.RemoveOldVersions) - - // We want cookies to be saved to disk so they are loaded the next time. - b.AddTeardownAction(b.CookieJar.PersistCookies) - - var frontendMode string - - switch { - case c.Bool(base.FlagCLI): - frontendMode = "cli" - case c.Bool(flagNonInteractive): - return <-(make(chan error)) // Block forever. - default: - frontendMode = "grpc" - } - - f := frontend.New( - frontendMode, - !c.Bool(base.FlagNoWindow), - b.CrashHandler, - b.Listener, - b.Updater, - bridge, - b, - b.Locations, - ) - - // Watch for updates routine - go func() { - ticker := time.NewTicker(constants.UpdateCheckInterval) - - for { - checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey)) - <-ticker.C - } - }() - - return f.Loop() -} - -func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { - log := logrus.WithField("pkg", "app/bridge") - version, err := u.Check() - if err != nil { - log.WithError(err).Error("An error occurred while checking for updates") - return - } - - f.WaitUntilFrontendIsReady() - - // Update links in UI - f.SetVersion(version) - - if !u.IsUpdateApplicable(version) { - log.Info("No need to update") - return - } - - log.WithField("version", version.Version).Info("An update is available") - - if !autoUpdate { - f.NotifyManualUpdate(version, u.CanInstall(version)) - return - } - - if !u.CanInstall(version) { - log.Info("A manual update is required") - f.NotifySilentUpdateError(updater.ErrManualUpdateRequired) - return - } - - if err := u.InstallUpdate(version); err != nil { - if errors.Cause(err) == updater.ErrDownloadVerify { - log.WithError(err).Warning("Skipping update installation due to temporary error") - } else { - log.WithError(err).Error("The update couldn't be installed") - f.NotifySilentUpdateError(err) - } - - return - } - - f.NotifySilentUpdateInstalled() -} - -// loadMessageCache loads local cache in case it is enabled in settings and available. -// In any other case it is returning in-memory cache. Could also return an error in case -// local cache is enabled but unavailable (in-memory cache will be returned nevertheless). -func loadMessageCache(b *base.Base) (cache.Cache, error) { - if !b.Settings.GetBool(settings.CacheEnabledKey) { - return cache.NewInMemoryCache(inMemoryCacheLimnit), nil - } - - var compressor cache.Compressor - - // NOTE(GODT-1158): Changing compression is not an option currently - // available for user but, if user changes compression setting we have - // to nuke the cache. - if b.Settings.GetBool(settings.CacheCompressionKey) { - compressor = &cache.GZipCompressor{} - } else { - compressor = &cache.NoopCompressor{} - } - - var path string - - if customPath := b.Settings.Get(settings.CacheLocationKey); customPath != "" { - path = customPath - } else { - path = b.Cache.GetDefaultMessageCacheDir() - // Store path so it will allways persist if default location - // will be changed in new version. - b.Settings.Set(settings.CacheLocationKey, path) - } - - // To prevent memory peaks we set maximal write concurency for store - // build jobs. - store.SetBuildAndCacheJobLimit(b.Settings.GetInt(settings.CacheConcurrencyWrite)) - - messageCache, err := cache.NewOnDiskCache(path, compressor, cache.Options{ - MinFreeAbs: uint64(b.Settings.GetInt(settings.CacheMinFreeAbsKey)), - MinFreeRat: b.Settings.GetFloat64(settings.CacheMinFreeRatKey), - ConcurrentRead: b.Settings.GetInt(settings.CacheConcurrencyRead), - ConcurrentWrite: b.Settings.GetInt(settings.CacheConcurrencyWrite), - }) - if err != nil { - return cache.NewInMemoryCache(inMemoryCacheLimnit), err - } - - return messageCache, nil -} diff --git a/internal/app/logging.go b/internal/app/logging.go new file mode 100644 index 00000000..253b595d --- /dev/null +++ b/internal/app/logging.go @@ -0,0 +1,28 @@ +package app + +import ( + "fmt" + + "github.com/ProtonMail/proton-bridge/v2/internal/crash" + "github.com/ProtonMail/proton-bridge/v2/internal/locations" + "github.com/ProtonMail/proton-bridge/v2/internal/logging" + "github.com/urfave/cli/v2" +) + +func initLogging(c *cli.Context, locations *locations.Locations, crashHandler *crash.Handler) error { + // Get a place to keep our logs. + logsPath, err := locations.ProvideLogsPath() + if err != nil { + return fmt.Errorf("could not provide logs path: %w", err) + } + + // Initialize logging. + if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil { + return fmt.Errorf("could not initialize logging: %w", err) + } + + // Ensure we dump a stack trace if we crash. + crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath)) + + return nil +} diff --git a/internal/bridge/autostart.go b/internal/bridge/autostart.go deleted file mode 100644 index 6d692b5a..00000000 --- a/internal/bridge/autostart.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package bridge provides core functionality of Bridge app. -package bridge - -import "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - -// IsAutostartEnabled checks if link file exits. -func (b *Bridge) IsAutostartEnabled() bool { - return b.autostart.IsEnabled() -} - -// EnableAutostart creates link and sets the preferences. -func (b *Bridge) EnableAutostart() error { - b.settings.SetBool(settings.AutostartKey, true) - return b.autostart.Enable() -} - -// DisableAutostart removes link and sets the preferences. -func (b *Bridge) DisableAutostart() error { - b.settings.SetBool(settings.AutostartKey, false) - return b.autostart.Disable() -} diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 1e05deae..533a9c58 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -1,325 +1,318 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package bridge provides core functionality of Bridge app. package bridge import ( - "errors" + "context" + "crypto/tls" "fmt" - "strconv" - "time" + "net" + "net/http" + "sync" "github.com/Masterminds/semver/v3" - "github.com/ProtonMail/go-autostart" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/config/tls" - "github.com/ProtonMail/proton-bridge/v2/internal/config/useragent" + "github.com/ProtonMail/gluon" + "github.com/ProtonMail/gluon/watcher" "github.com/ProtonMail/proton-bridge/v2/internal/constants" - "github.com/ProtonMail/proton-bridge/v2/internal/metrics" - "github.com/ProtonMail/proton-bridge/v2/internal/sentry" - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" - "github.com/ProtonMail/proton-bridge/v2/internal/updater" - "github.com/ProtonMail/proton-bridge/v2/internal/users" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - logrus "github.com/sirupsen/logrus" + "github.com/ProtonMail/proton-bridge/v2/internal/cookies" + "github.com/ProtonMail/proton-bridge/v2/internal/events" + "github.com/ProtonMail/proton-bridge/v2/internal/focus" + "github.com/ProtonMail/proton-bridge/v2/internal/user" + "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/bradenaw/juniper/xslices" + "github.com/emersion/go-smtp" + "github.com/go-resty/resty/v2" + "github.com/sirupsen/logrus" + "gitlab.protontech.ch/go/liteapi" ) -var log = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals - -var ErrLocalCacheUnavailable = errors.New("local cache is unavailable") - type Bridge struct { - *users.Users + // vault holds bridge-specific data, such as preferences and known users (authorized or not). + vault *vault.Vault - locations Locator - settings SettingsProvider - clientManager pmapi.Manager + // users holds authorized users. + users map[string]*user.User + + // api manages user API clients. + api *liteapi.Manager + cookieJar *cookies.Jar + proxyDialer ProxyDialer + identifier Identifier + + // watchers holds all registered event watchers. + watchers []*watcher.Watcher[events.Event] + watchersLock sync.RWMutex + + // tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers. + tlsConfig *tls.Config + + // imapServer is the bridge's IMAP server. + imapServer *gluon.Server + imapListener net.Listener + + // smtpServer is the bridge's SMTP server. + smtpServer *smtp.Server + smtpBackend *smtpBackend + smtpListener net.Listener + + // updater is the bridge's updater. updater Updater - versioner Versioner - tls *tls.TLS - userAgent *useragent.UserAgent - cacheProvider CacheProvider - autostart *autostart.App - // Bridge's global errors list. - errors []error + curVersion *semver.Version + updateCheckCh chan struct{} - isAllMailVisible bool - isFirstStart bool - lastVersion string + // focusService is used to raise the bridge window when needed. + focusService *focus.FocusService + + // autostarter is the bridge's autostarter. + autostarter Autostarter + + // locator is the bridge's locator. + locator Locator + + // errors contains errors encountered during startup. + errors []error } -func New( //nolint:funlen - locations Locator, - cacheProvider CacheProvider, - setting SettingsProvider, - sentryReporter *sentry.Reporter, - panicHandler users.PanicHandler, - eventListener listener.Listener, - tls *tls.TLS, - userAgent *useragent.UserAgent, - cache cache.Cache, - builder *message.Builder, - clientManager pmapi.Manager, - credStorer users.CredentialsStorer, - updater Updater, - versioner Versioner, - autostart *autostart.App, -) *Bridge { - // Allow DoH before starting the app if the user has previously set this setting. - // This allows us to start even if protonmail is blocked. - if setting.GetBool(settings.AllowProxyKey) { - clientManager.AllowProxy() +// New creates a new bridge. +func New( + apiURL string, // the URL of the API to use + locator Locator, // the locator to provide paths to store data + vault *vault.Vault, // the bridge's encrypted data store + identifier Identifier, // the identifier to keep track of the user agent + tlsReporter TLSReporter, // the TLS reporter to report TLS errors + proxyDialer ProxyDialer, // the DoH dialer + autostarter Autostarter, // the autostarter to manage autostart settings + updater Updater, // the updater to fetch and install updates + curVersion *semver.Version, // the current version of the bridge +) (*Bridge, error) { + if vault.GetProxyAllowed() { + proxyDialer.AllowProxy() + } else { + proxyDialer.DisallowProxy() } - u := users.New( - locations, - panicHandler, - eventListener, - clientManager, - credStorer, - newStoreFactory(cacheProvider, sentryReporter, panicHandler, eventListener, cache, builder), + cookieJar, err := cookies.NewCookieJar(vault) + if err != nil { + return nil, fmt.Errorf("failed to create cookie jar: %w", err) + } + + api := liteapi.New( + liteapi.WithHostURL(apiURL), + liteapi.WithAppVersion(constants.AppVersion), + liteapi.WithCookieJar(cookieJar), + liteapi.WithTransport(&http.Transport{DialTLSContext: proxyDialer.DialTLSContext}), ) - b := &Bridge{ - Users: u, - locations: locations, - settings: setting, - clientManager: clientManager, - updater: updater, - versioner: versioner, - tls: tls, - userAgent: userAgent, - cacheProvider: cacheProvider, - autostart: autostart, - isFirstStart: false, - isAllMailVisible: setting.GetBool(settings.IsAllMailVisible), - } - - if setting.GetBool(settings.FirstStartKey) { - b.isFirstStart = true - if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil { - logrus.WithError(err).Error("Failed to send metric") - } - setting.SetBool(settings.FirstStartKey, false) - } - - // Keep in bridge and update in settings the last used version. - b.lastVersion = b.settings.Get(settings.LastVersionKey) - b.settings.Set(settings.LastVersionKey, constants.Version) - - go b.heartbeat() - - return b -} - -// heartbeat sends a heartbeat signal once a day. -func (b *Bridge) heartbeat() { - for range time.Tick(time.Minute) { - lastHeartbeatDay, err := strconv.ParseInt(b.settings.Get(settings.LastHeartbeatKey), 10, 64) - if err != nil { - continue - } - - // If we're still on the same day, don't send a heartbeat. - if time.Now().YearDay() == int(lastHeartbeatDay) { - continue - } - - // We're on the next (or a different) day, so send a heartbeat. - if err := b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel)); err != nil { - logrus.WithError(err).Error("Failed to send heartbeat") - continue - } - - // Heartbeat was sent successfully so update the last heartbeat day. - b.settings.Set(settings.LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay())) - } -} - -// GetUpdateChannel returns currently set update channel. -func (b *Bridge) GetUpdateChannel() updater.UpdateChannel { - return updater.UpdateChannel(b.settings.Get(settings.UpdateChannelKey)) -} - -// SetUpdateChannel switches update channel. -func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) { - b.settings.Set(settings.UpdateChannelKey, string(channel)) -} - -func (b *Bridge) resetToLatestStable() error { - version, err := b.updater.Check() + tlsConfig, err := loadTLSConfig(vault) if err != nil { - // If we can not check for updates - just remove all local updates and reset to base installer version. - // Not using `b.locations.ClearUpdates()` because `versioner.RemoveOtherVersions` can also handle - // case when it is needed to remove currently running verion. - if err := b.versioner.RemoveOtherVersions(semver.MustParse("0.0.0")); err != nil { - log.WithError(err).Error("Failed to clear updates while downgrading channel") - } + return nil, fmt.Errorf("failed to load TLS config: %w", err) + } + + imapServer, err := newIMAPServer(vault.GetGluonDir(), curVersion, tlsConfig) + if err != nil { + return nil, fmt.Errorf("failed to create IMAP server: %w", err) + } + + smtpBackend, err := newSMTPBackend() + if err != nil { + return nil, fmt.Errorf("failed to create SMTP backend: %w", err) + } + + smtpServer, err := newSMTPServer(smtpBackend, tlsConfig) + if err != nil { + return nil, fmt.Errorf("failed to create SMTP server: %w", err) + } + + focusService, err := focus.NewService() + if err != nil { + return nil, fmt.Errorf("failed to create focus service: %w", err) + } + + bridge := &Bridge{ + vault: vault, + users: make(map[string]*user.User), + + api: api, + cookieJar: cookieJar, + proxyDialer: proxyDialer, + identifier: identifier, + + tlsConfig: tlsConfig, + imapServer: imapServer, + smtpServer: smtpServer, + smtpBackend: smtpBackend, + + updater: updater, + curVersion: curVersion, + updateCheckCh: make(chan struct{}, 1), + + focusService: focusService, + autostarter: autostarter, + locator: locator, + } + + api.AddStatusObserver(func(status liteapi.Status) { + bridge.publish(events.ConnStatus{ + Status: status, + }) + }) + + api.AddErrorHandler(liteapi.AppVersionBadCode, func() { + bridge.publish(events.UpdateForced{}) + }) + + api.AddPreRequestHook(func(_ *resty.Client, req *resty.Request) error { + req.SetHeader("User-Agent", bridge.identifier.GetUserAgent()) return nil + }) + + go func() { + for range tlsReporter.GetTLSIssueCh() { + bridge.publish(events.TLSIssue{}) + } + }() + + go func() { + for range focusService.GetRaiseCh() { + bridge.publish(events.Raise{}) + } + }() + + go func() { + for event := range imapServer.AddWatcher() { + bridge.handleIMAPEvent(event) + } + }() + + if err := bridge.loadUsers(context.Background()); err != nil { + return nil, fmt.Errorf("failed to load connected users: %w", err) } - // If current version is same as upstream stable version - do nothing. - if version.Version.Equal(semver.MustParse(constants.Version)) { - return nil + if err := bridge.serveIMAP(); err != nil { + bridge.PushError(ErrServeIMAP) } - if err := b.updater.InstallUpdate(version); err != nil { - return err + if err := bridge.serveSMTP(); err != nil { + bridge.PushError(ErrServeSMTP) } - return b.versioner.RemoveOtherVersions(version.Version) + if err := bridge.watchForUpdates(); err != nil { + bridge.PushError(ErrWatchUpdates) + } + + return bridge, nil } -// FactoryReset will remove all local cache and settings. -// It will also downgrade to latest stable version if user is on early version. -func (b *Bridge) FactoryReset() { - wasEarly := b.GetUpdateChannel() == updater.EarlyChannel +// GetEvents returns a channel of events of the given type. +// If no types are supplied, all events are returned. +func (bridge *Bridge) GetEvents(ofType ...events.Event) (<-chan events.Event, func()) { + newWatcher := bridge.addWatcher(ofType...) - b.settings.Set(settings.UpdateChannelKey, string(updater.StableChannel)) + return newWatcher.GetChannel(), func() { bridge.remWatcher(newWatcher) } +} - if wasEarly { - if err := b.resetToLatestStable(); err != nil { - log.WithError(err).Error("Failed to reset to latest stable version") +func (bridge *Bridge) FactoryReset(ctx context.Context) error { + panic("TODO") +} + +func (bridge *Bridge) PushError(err error) { + bridge.errors = append(bridge.errors, err) +} + +func (bridge *Bridge) GetErrors() []error { + return bridge.errors +} + +func (bridge *Bridge) Close(ctx context.Context) error { + // Close the IMAP server. + if err := bridge.closeIMAP(ctx); err != nil { + logrus.WithError(err).Error("Failed to close IMAP server") + } + + // Close the SMTP server. + if err := bridge.closeSMTP(); err != nil { + logrus.WithError(err).Error("Failed to close SMTP server") + } + + // Close all users. + for _, user := range bridge.users { + if err := user.Close(ctx); err != nil { + logrus.WithError(err).Error("Failed to close user") } } - if err := b.Users.ClearData(); err != nil { - log.WithError(err).Error("Failed to remove bridge data") + // Persist the cookies. + if err := bridge.cookieJar.PersistCookies(); err != nil { + logrus.WithError(err).Error("Failed to persist cookies") } - if err := b.Users.ClearUsers(); err != nil { - log.WithError(err).Error("Failed to remove bridge users") + // Close the focus service. + bridge.focusService.Close() + + // Save the last version of bridge that was run. + if err := bridge.vault.SetLastVersion(bridge.curVersion); err != nil { + logrus.WithError(err).Error("Failed to save last version") } -} - -// GetKeychainApp returns current keychain helper. -func (b *Bridge) GetKeychainApp() string { - return b.settings.Get(settings.PreferredKeychainKey) -} - -// SetKeychainApp sets current keychain helper. -func (b *Bridge) SetKeychainApp(helper string) { - b.settings.Set(settings.PreferredKeychainKey, helper) -} - -func (b *Bridge) EnableCache() error { - if err := b.Users.EnableCache(); err != nil { - return err - } - - b.settings.SetBool(settings.CacheEnabledKey, true) return nil } -func (b *Bridge) DisableCache() error { - if err := b.Users.DisableCache(); err != nil { - return err - } +func (bridge *Bridge) publish(event events.Event) { + bridge.watchersLock.RLock() + defer bridge.watchersLock.RUnlock() - b.settings.SetBool(settings.CacheEnabledKey, false) - // Reset back to the default location when disabling. - b.settings.Set(settings.CacheLocationKey, b.cacheProvider.GetDefaultMessageCacheDir()) - - return nil -} - -func (b *Bridge) MigrateCache(from, to string) error { - if err := b.Users.MigrateCache(from, to); err != nil { - return err - } - - b.settings.Set(settings.CacheLocationKey, to) - - return nil -} - -// SetProxyAllowed instructs the app whether to use DoH to access an API proxy if necessary. -// It also needs to work before the app is initialised (because we may need to use the proxy at startup). -func (b *Bridge) SetProxyAllowed(proxyAllowed bool) { - b.settings.SetBool(settings.AllowProxyKey, proxyAllowed) - if proxyAllowed { - b.clientManager.AllowProxy() - } else { - b.clientManager.DisallowProxy() - } -} - -// GetProxyAllowed returns whether use of DoH is enabled to access an API proxy if necessary. -func (b *Bridge) GetProxyAllowed() bool { - return b.settings.GetBool(settings.AllowProxyKey) -} - -// AddError add an error to a global error list if it does not contain it yet. Adding nil is noop. -func (b *Bridge) AddError(err error) { - if err == nil { - return - } - if b.HasError(err) { - return - } - - b.errors = append(b.errors, err) -} - -// DelError removes an error from global error list. -func (b *Bridge) DelError(err error) { - for idx, val := range b.errors { - if val == err { - b.errors = append(b.errors[:idx], b.errors[idx+1:]...) - return + for _, watcher := range bridge.watchers { + if watcher.IsWatching(event) { + if ok := watcher.Send(event); !ok { + logrus.WithField("event", event).Warn("Failed to send event to watcher") + } } } } -// HasError returnes true if global error list contains an err. -func (b *Bridge) HasError(err error) bool { - for _, val := range b.errors { - if val == err { - return true - } +func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events.Event] { + bridge.watchersLock.Lock() + defer bridge.watchersLock.Unlock() + + newWatcher := watcher.New(ofType...) + + bridge.watchers = append(bridge.watchers, newWatcher) + + return newWatcher +} + +func (bridge *Bridge) remWatcher(oldWatcher *watcher.Watcher[events.Event]) { + bridge.watchersLock.Lock() + defer bridge.watchersLock.Unlock() + + bridge.watchers = xslices.Filter(bridge.watchers, func(other *watcher.Watcher[events.Event]) bool { + return other != oldWatcher + }) +} + +func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) { + cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey()) + if err != nil { + return nil, err } - return false + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, nil } -// GetLastVersion returns the version which was used in previous execution of -// Bridge. -func (b *Bridge) GetLastVersion() string { - return b.lastVersion -} +func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) { + if useTLS { + tlsListener, err := tls.Listen("tcp", fmt.Sprintf(":%v", port), tlsConfig) + if err != nil { + return nil, err + } -// IsFirstStart returns true when Bridge is running for first time or after -// factory reset. -func (b *Bridge) IsFirstStart() bool { - return b.isFirstStart -} + return tlsListener, nil + } -// IsAllMailVisible can be called extensively by IMAP. Therefore, it is better -// to cache the value instead of reading from settings file. -func (b *Bridge) IsAllMailVisible() bool { - return b.isAllMailVisible -} + netListener, err := net.Listen("tcp", fmt.Sprintf(":%v", port)) + if err != nil { + return nil, err + } -func (b *Bridge) SetIsAllMailVisible(isVisible bool) { - b.settings.SetBool(settings.IsAllMailVisible, isVisible) - b.isAllMailVisible = isVisible + return netListener, nil } diff --git a/internal/bridge/bridge_test.go b/internal/bridge/bridge_test.go new file mode 100644 index 00000000..6e6fd3ef --- /dev/null +++ b/internal/bridge/bridge_test.go @@ -0,0 +1,362 @@ +package bridge_test + +import ( + "context" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" + "github.com/ProtonMail/proton-bridge/v2/internal/events" + "github.com/ProtonMail/proton-bridge/v2/internal/focus" + "github.com/ProtonMail/proton-bridge/v2/internal/locations" + "github.com/ProtonMail/proton-bridge/v2/internal/updater" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" + "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/bradenaw/juniper/xslices" + "github.com/stretchr/testify/require" + "gitlab.protontech.ch/go/liteapi" + "gitlab.protontech.ch/go/liteapi/server" +) + +const ( + username = "username" + password = "password" +) + +var ( + v2_3_0 = semver.MustParse("2.3.0") + v2_4_0 = semver.MustParse("2.4.0") +) + +func TestBridge_ConnStatus(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Get a stream of connection status events. + eventCh, done := bridge.GetEvents(events.ConnStatus{}) + defer done() + + // Simulate network disconnect. + mocks.TLSDialer.SetCanDial(false) + + // Trigger some operation that will fail due to the network disconnect. + _, err := bridge.LoginUser(context.Background(), username, password, nil, nil) + require.Error(t, err) + + // Wait for the event. + require.Equal(t, events.ConnStatus{Status: liteapi.StatusDown}, <-eventCh) + + // Simulate network reconnect. + mocks.TLSDialer.SetCanDial(true) + + // Trigger some operation that will succeed due to the network reconnect. + userID, err := bridge.LoginUser(context.Background(), username, password, nil, nil) + require.NoError(t, err) + require.NotEmpty(t, userID) + + // Wait for the event. + require.Equal(t, events.ConnStatus{Status: liteapi.StatusUp}, <-eventCh) + }) + }) +} + +func TestBridge_TLSIssue(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Get a stream of TLS issue events. + tlsEventCh, done := bridge.GetEvents(events.TLSIssue{}) + defer done() + + // Simulate a TLS issue. + go func() { + mocks.TLSIssueCh <- struct{}{} + }() + + // Wait for the event. + require.IsType(t, events.TLSIssue{}, <-tlsEventCh) + }) + }) +} + +func TestBridge_Focus(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Get a stream of TLS issue events. + raiseCh, done := bridge.GetEvents(events.Raise{}) + defer done() + + // Simulate a focus event. + focus.TryRaise() + + // Wait for the event. + require.IsType(t, events.Raise{}, <-raiseCh) + }) + }) +} + +func TestBridge_UserAgent(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + var calls []server.Call + + s.AddCallWatcher(func(call server.Call) { + calls = append(calls, call) + }) + + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Set the platform to something other than the default. + bridge.SetCurrentPlatform("platform") + + // Assert that the user agent then contains the platform. + require.Contains(t, bridge.GetCurrentUserAgent(), "platform") + + // Login the user. + _, err := bridge.LoginUser(context.Background(), username, password, nil, nil) + require.NoError(t, err) + + // Assert that the user agent was sent to the API. + require.Contains(t, calls[len(calls)-1].Request.Header.Get("User-Agent"), bridge.GetCurrentUserAgent()) + }) + }) +} + +func TestBridge_Cookies(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + var calls []server.Call + + s.AddCallWatcher(func(call server.Call) { + calls = append(calls, call) + }) + + var sessionID string + + // Start bridge and add a user so that API assigns us a session ID via cookie. + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + _, err := bridge.LoginUser(context.Background(), username, password, nil, nil) + require.NoError(t, err) + + cookie, err := calls[len(calls)-1].Request.Cookie("Session-Id") + require.NoError(t, err) + + sessionID = cookie.Value + }) + + // Start bridge again and check that it uses the same session ID. + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + cookie, err := calls[len(calls)-1].Request.Cookie("Session-Id") + require.NoError(t, err) + + require.Equal(t, sessionID, cookie.Value) + }) + }) +} + +func TestBridge_CheckUpdate(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Disable autoupdate for this test. + require.NoError(t, bridge.SetAutoUpdate(false)) + + // Get a stream of update events. + updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{}, events.UpdateAvailable{}) + defer done() + + // We are currently on the latest version. + bridge.CheckForUpdates() + require.Equal(t, events.UpdateNotAvailable{}, <-updateCh) + + // Simulate a new version being available. + mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0) + + // Check for updates. + bridge.CheckForUpdates() + require.Equal(t, events.UpdateAvailable{ + Version: updater.VersionInfo{ + Version: v2_4_0, + MinAuto: v2_3_0, + RolloutProportion: 1.0, + }, + CanInstall: true, + }, <-updateCh) + }) + }) +} + +func TestBridge_AutoUpdate(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Enable autoupdate for this test. + require.NoError(t, bridge.SetAutoUpdate(true)) + + // Get a stream of update events. + updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{}, events.UpdateInstalled{}) + defer done() + + // Simulate a new version being available. + mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0) + + // Check for updates. + bridge.CheckForUpdates() + require.Equal(t, events.UpdateInstalled{ + Version: updater.VersionInfo{ + Version: v2_4_0, + MinAuto: v2_3_0, + RolloutProportion: 1.0, + }, + }, <-updateCh) + }) + }) +} + +func TestBridge_ManualUpdate(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Disable autoupdate for this test. + require.NoError(t, bridge.SetAutoUpdate(false)) + + // Get a stream of update events. + updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{}, events.UpdateAvailable{}) + defer done() + + // Simulate a new version being available, but it's too new for us. + mocks.Updater.SetLatestVersion(v2_4_0, v2_4_0) + + // Check for updates. + bridge.CheckForUpdates() + require.Equal(t, events.UpdateAvailable{ + Version: updater.VersionInfo{ + Version: v2_4_0, + MinAuto: v2_4_0, + RolloutProportion: 1.0, + }, + CanInstall: false, + }, <-updateCh) + }) + }) +} + +func TestBridge_ForceUpdate(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Get a stream of update events. + updateCh, done := bridge.GetEvents(events.UpdateForced{}) + defer done() + + // Set the minimum accepted app version to something newer than the current version. + s.SetMinAppVersion(v2_4_0) + + // Try to login the user. It will fail because the bridge is too old. + _, err := bridge.LoginUser(context.Background(), username, password, nil, nil) + require.Error(t, err) + + // We should get an update required event. + require.Equal(t, events.UpdateForced{}, <-updateCh) + }) + }) +} + +func TestBridge_BadVaultKey(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, vaultKey []byte) { + var userID string + + // Login a user. + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + newUserID, err := bridge.LoginUser(context.Background(), username, password, nil, nil) + require.NoError(t, err) + + userID = newUserID + }) + + // Start bridge with the correct vault key -- it should load the users correctly. + withBridge(t, s.GetHostURL(), locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + require.ElementsMatch(t, []string{userID}, bridge.GetUserIDs()) + }) + + // Start bridge with a bad vault key, the vault will be wiped and bridge will show no users. + withBridge(t, s.GetHostURL(), locator, []byte("bad"), func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + require.Empty(t, bridge.GetUserIDs()) + }) + + // Start bridge with a nil vault key, the vault will be wiped and bridge will show no users. + withBridge(t, s.GetHostURL(), locator, nil, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + require.Empty(t, bridge.GetUserIDs()) + }) + }) +} + +// withEnv creates the full test environment and runs the tests. +func withEnv(t *testing.T, tests func(server *server.Server, locator bridge.Locator, vaultKey []byte)) { + // Create test API. + server := server.NewTLS() + defer server.Close() + + // Add test user. + _, _, err := server.AddUser(username, password, username+"@pm.me") + require.NoError(t, err) + + // Generate a random vault key. + vaultKey, err := crypto.RandomToken(32) + require.NoError(t, err) + + // Run the tests. + tests(server, locations.New(bridge.NewTestLocationsProvider(t), "config-name"), vaultKey) +} + +// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done. +func withBridge(t *testing.T, apiURL string, locator bridge.Locator, vaultKey []byte, tests func(bridge *bridge.Bridge, mocks *bridge.Mocks)) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create the mock objects used in the tests. + mocks := bridge.NewMocks(t, v2_3_0, v2_3_0) + + // Bridge will enable the proxy by default at startup. + mocks.ProxyDialer.EXPECT().AllowProxy() + + // Get the path to the vault. + vaultDir, err := locator.ProvideSettingsPath() + require.NoError(t, err) + + // Create the vault. + vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey) + require.NoError(t, err) + + // Create a new bridge. + bridge, err := bridge.New( + apiURL, + locator, + vault, + useragent.New(), + mocks.TLSReporter, + mocks.ProxyDialer, + mocks.Autostarter, + mocks.Updater, + v2_3_0, + ) + require.NoError(t, err) + + // Use the bridge. + tests(bridge, mocks) + + // Close the bridge. + require.NoError(t, bridge.Close(ctx)) +} + +// must is a helper function that panics on error. +func must[T any](val T, err error) T { + if err != nil { + panic(err) + } + + return val +} + +func getConnectedUserIDs(t *testing.T, bridge *bridge.Bridge) []string { + t.Helper() + + return xslices.Filter(bridge.GetUserIDs(), func(userID string) bool { + info, err := bridge.GetUserInfo(userID) + require.NoError(t, err) + + return info.Connected + }) +} diff --git a/internal/bridge/bug_report.go b/internal/bridge/bug_report.go index fa728d3f..24a3a316 100644 --- a/internal/bridge/bug_report.go +++ b/internal/bridge/bug_report.go @@ -21,67 +21,51 @@ import ( "archive/zip" "bytes" "context" - "errors" "io" "os" "path/filepath" "sort" "github.com/ProtonMail/proton-bridge/v2/internal/logging" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" + "gitlab.protontech.ch/go/liteapi" ) const ( - MaxAttachmentSize = 7 * 1024 * 1024 // MaxAttachmentSize 7 MB total limit + MaxAttachmentSize = 7 * (1 << 20) // MaxAttachmentSize 7 MB total size of all attachments. MaxCompressedFilesCount = 6 ) -var ErrSizeTooLarge = errors.New("file is too big") +func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { + var account string -// ReportBug reports a new bug from the user. -func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error { //nolint:funlen - if user, err := b.GetUser(address); err == nil { - accountName = user.Username() - } else if users := b.GetUsers(); len(users) > 0 { - accountName = users[0].Username() + if info, err := bridge.QueryUserInfo(username); err == nil { + account = info.Username + } else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 { + account = bridge.users[userIDs[0]].Name() } - report := pmapi.ReportBugReq{ - OS: osType, - OSVersion: osVersion, - Browser: emailClient, - Title: "[Bridge] Bug", - Description: description, - Username: accountName, - Email: address, - } + var atts []liteapi.ReportBugAttachment if attachLogs { - logs, err := b.getMatchingLogs( - func(filename string) bool { - return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename) - }, - ) + logs, err := getMatchingLogs(bridge.locator, func(filename string) bool { + return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename) + }) if err != nil { - log.WithError(err).Error("Can't get log files list") + return err } - guiLogs, err := b.getMatchingLogs( - func(filename string) bool { - return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename) - }, - ) + guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool { + return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename) + }) if err != nil { - log.WithError(err).Error("Can't get GUI log files list") + return err } - crashes, err := b.getMatchingLogs( - func(filename string) bool { - return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename) - }, - ) + crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool { + return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename) + }) if err != nil { - log.WithError(err).Error("Can't get crash files list") + return err } var matchFiles []string @@ -95,26 +79,42 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, archive, err := zipFiles(matchFiles) if err != nil { - log.WithError(err).Error("Can't zip logs and crashes") + return err } - if archive != nil { - report.AddAttachment("logs.zip", "application/zip", archive) + body, err := io.ReadAll(archive) + if err != nil { + return err } + + atts = append(atts, liteapi.ReportBugAttachment{ + Name: "logs.zip", + Filename: "logs.zip", + MIMEType: "application/zip", + Body: body, + }) } - return b.clientManager.ReportBug(context.Background(), report) + return bridge.api.ReportBug(ctx, liteapi.ReportBugReq{ + OS: osType, + OSVersion: osVersion, + Description: description, + Client: client, + Username: account, + Email: email, + }, atts...) } func max(a, b int) int { if a > b { return a } + return b } -func (b *Bridge) getMatchingLogs(filenameMatchFunc func(string) bool) (filenames []string, err error) { - logsPath, err := b.locations.ProvideLogsPath() +func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) { + logsPath, err := locator.ProvideLogsPath() if err != nil { return nil, err } @@ -131,24 +131,25 @@ func (b *Bridge) getMatchingLogs(filenameMatchFunc func(string) bool) (filenames matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name())) } } + sort.Strings(matchFiles) // Sorted by timestamp: oldest first. return matchFiles, nil } -type LimitedBuffer struct { +type limitedBuffer struct { capacity int buf *bytes.Buffer } -func NewLimitedBuffer(capacity int) *LimitedBuffer { - return &LimitedBuffer{ +func newLimitedBuffer(capacity int) *limitedBuffer { + return &limitedBuffer{ capacity: capacity, buf: bytes.NewBuffer(make([]byte, 0, capacity)), } } -func (b *LimitedBuffer) Write(p []byte) (n int, err error) { +func (b *limitedBuffer) Write(p []byte) (n int, err error) { if len(p)+b.buf.Len() > b.capacity { return 0, ErrSizeTooLarge } @@ -156,7 +157,7 @@ func (b *LimitedBuffer) Write(p []byte) (n int, err error) { return b.buf.Write(p) } -func (b *LimitedBuffer) Read(p []byte) (n int, err error) { +func (b *limitedBuffer) Read(p []byte) (n int, err error) { return b.buf.Read(p) } @@ -165,14 +166,13 @@ func zipFiles(filenames []string) (io.Reader, error) { return nil, nil } - buf := NewLimitedBuffer(MaxAttachmentSize) + buf := newLimitedBuffer(MaxAttachmentSize) w := zip.NewWriter(buf) defer w.Close() //nolint:errcheck for _, file := range filenames { - err := addFileToZip(file, w) - if err != nil { + if err := addFileToZip(file, w); err != nil { return nil, err } } @@ -209,12 +209,9 @@ func addFileToZip(filename string, writer *zip.Writer) error { return err } - _, err = io.Copy(fileWriter, fileReader) - if err != nil { + if _, err := io.Copy(fileWriter, fileReader); err != nil { return err } - err = fileReader.Close() - - return err + return fileReader.Close() } diff --git a/internal/bridge/configure.go b/internal/bridge/configure.go index 2e0bff68..0b239db8 100644 --- a/internal/bridge/configure.go +++ b/internal/bridge/configure.go @@ -1,70 +1,38 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - package bridge import ( "strings" "github.com/ProtonMail/proton-bridge/v2/internal/clientconfig" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/config/useragent" + "github.com/ProtonMail/proton-bridge/v2/internal/constants" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" ) -func (b *Bridge) ConfigureAppleMail(userID, address string) (bool, error) { - user, err := b.GetUser(userID) - if err != nil { - return false, err +func (bridge *Bridge) ConfigureAppleMail(userID, address string) error { + user, ok := bridge.users[userID] + if !ok { + return ErrNoSuchUser } if address == "" { - address = user.GetPrimaryAddress() + address = user.Addresses()[0] } - username := address - addresses := address - - if user.IsCombinedAddressMode() { - username = user.GetPrimaryAddress() - addresses = strings.Join(user.GetAddresses(), ",") - } - - var ( - restart = false - smtpSSL = b.settings.GetBool(settings.SMTPSSLKey) - ) - // If configuring apple mail for Catalina or newer, users should use SSL. - if useragent.IsCatalinaOrNewer() && !smtpSSL { - smtpSSL = true - restart = true - b.settings.SetBool(settings.SMTPSSLKey, true) + if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() { + if err := bridge.SetSMTPSSL(true); err != nil { + return err + } } - if err := (&clientconfig.AppleMail{}).Configure( - Host, - b.settings.GetInt(settings.IMAPPortKey), - b.settings.GetInt(settings.SMTPPortKey), - false, smtpSSL, - username, addresses, - user.GetBridgePassword(), - ); err != nil { - return false, err - } - - return restart, nil + return (&clientconfig.AppleMail{}).Configure( + constants.Host, + bridge.vault.GetIMAPPort(), + bridge.vault.GetSMTPPort(), + bridge.vault.GetIMAPSSL(), + bridge.vault.GetSMTPSSL(), + address, + strings.Join(user.Addresses(), ","), + user.BridgePass(), + ) } diff --git a/internal/bridge/constants.go b/internal/bridge/constants.go deleted file mode 100644 index d7ed8bd4..00000000 --- a/internal/bridge/constants.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package bridge - -// Host settings. -const ( - Host = "127.0.0.1" -) diff --git a/internal/bridge/errors.go b/internal/bridge/errors.go new file mode 100644 index 00000000..d7d1e25c --- /dev/null +++ b/internal/bridge/errors.go @@ -0,0 +1,16 @@ +package bridge + +import "errors" + +var ( + ErrServeIMAP = errors.New("failed to serve IMAP") + ErrServeSMTP = errors.New("failed to serve SMTP") + ErrWatchUpdates = errors.New("failed to watch for updates") + + ErrNoSuchUser = errors.New("no such user") + ErrUserAlreadyExists = errors.New("user already exists") + ErrUserAlreadyLoggedIn = errors.New("user already logged in") + ErrNotImplemented = errors.New("not implemented") + + ErrSizeTooLarge = errors.New("file is too big") +) diff --git a/internal/bridge/files.go b/internal/bridge/files.go new file mode 100644 index 00000000..2a272d49 --- /dev/null +++ b/internal/bridge/files.go @@ -0,0 +1,67 @@ +package bridge + +import ( + "os" + "path/filepath" +) + +func moveDir(from, to string) error { + entries, err := os.ReadDir(from) + if err != nil { + return err + } + + for _, entry := range entries { + if entry.IsDir() { + if err := os.Mkdir(filepath.Join(to, entry.Name()), 0700); err != nil { + return err + } + + if err := moveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil { + return err + } + + if err := os.RemoveAll(filepath.Join(from, entry.Name())); err != nil { + return err + } + } else { + if err := move(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil { + return err + } + } + } + + return os.Remove(from) +} + +func move(from, to string) error { + if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil { + return err + } + + f, err := os.Open(from) + if err != nil { + return err + } + defer f.Close() + + c, err := os.Create(to) + if err != nil { + return err + } + defer c.Close() + + if err := os.Chmod(to, 0600); err != nil { + return err + } + + if _, err := c.ReadFrom(f); err != nil { + return err + } + + if err := os.Remove(from); err != nil { + return err + } + + return nil +} diff --git a/internal/bridge/files_test.go b/internal/bridge/files_test.go new file mode 100644 index 00000000..e8152810 --- /dev/null +++ b/internal/bridge/files_test.go @@ -0,0 +1,56 @@ +package bridge + +import ( + "os" + "path/filepath" + "testing" +) + +func TestMoveDir(t *testing.T) { + from, to := t.TempDir(), t.TempDir() + + // Create some files in from. + if err := os.WriteFile(filepath.Join(from, "a"), []byte("a"), 0600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(from, "b"), []byte("b"), 0600); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(filepath.Join(from, "c"), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(from, "c", "d"), []byte("d"), 0600); err != nil { + t.Fatal(err) + } + + // Move the files. + if err := moveDir(from, to); err != nil { + t.Fatal(err) + } + + // Check that the files were moved. + if _, err := os.Stat(filepath.Join(from, "a")); !os.IsNotExist(err) { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(to, "a")); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(from, "b")); !os.IsNotExist(err) { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(to, "b")); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(from, "c")); !os.IsNotExist(err) { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(to, "c")); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(from, "c", "d")); !os.IsNotExist(err) { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(to, "c", "d")); err != nil { + t.Fatal(err) + } +} diff --git a/internal/bridge/imap.go b/internal/bridge/imap.go new file mode 100644 index 00000000..b247260a --- /dev/null +++ b/internal/bridge/imap.go @@ -0,0 +1,117 @@ +package bridge + +import ( + "context" + "crypto/tls" + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/gluon" + imapEvents "github.com/ProtonMail/gluon/events" + "github.com/ProtonMail/proton-bridge/v2/internal/constants" + "github.com/sirupsen/logrus" +) + +const ( + defaultClientName = "UnknownClient" + defaultClientVersion = "0.0.1" +) + +func (bridge *Bridge) GetIMAPPort() int { + return bridge.vault.GetIMAPPort() +} + +func (bridge *Bridge) SetIMAPPort(newPort int) error { + if newPort == bridge.vault.GetIMAPPort() { + return nil + } + + if err := bridge.vault.SetIMAPPort(newPort); err != nil { + return err + } + + return bridge.restartIMAP(context.Background()) +} + +func (bridge *Bridge) GetIMAPSSL() bool { + return bridge.vault.GetIMAPSSL() +} + +func (bridge *Bridge) SetIMAPSSL(newSSL bool) error { + if newSSL == bridge.vault.GetIMAPSSL() { + return nil + } + + if err := bridge.vault.SetIMAPSSL(newSSL); err != nil { + return err + } + + return bridge.restartIMAP(context.Background()) +} + +func (bridge *Bridge) serveIMAP() error { + imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig) + if err != nil { + return fmt.Errorf("failed to create IMAP listener: %w", err) + } + + bridge.imapListener = imapListener + + return bridge.imapServer.Serve(context.Background(), bridge.imapListener) +} + +func (bridge *Bridge) restartIMAP(ctx context.Context) error { + if err := bridge.imapListener.Close(); err != nil { + logrus.WithError(err).Warn("Failed to close IMAP listener") + } + + return bridge.serveIMAP() +} + +func (bridge *Bridge) closeIMAP(ctx context.Context) error { + if err := bridge.imapServer.Close(ctx); err != nil { + logrus.WithError(err).Warn("Failed to close IMAP server") + } + + if err := bridge.imapListener.Close(); err != nil { + logrus.WithError(err).Warn("Failed to close IMAP listener") + } + + return nil +} + +func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) { + switch event := event.(type) { + case imapEvents.SessionAdded: + if !bridge.identifier.HasClient() { + bridge.identifier.SetClient(defaultClientName, defaultClientVersion) + } + + case imapEvents.IMAPID: + bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version) + } +} + +func newIMAPServer(gluonDir string, version *semver.Version, tlsConfig *tls.Config) (*gluon.Server, error) { + imapServer, err := gluon.New( + gluon.WithTLS(tlsConfig), + gluon.WithDataDir(gluonDir), + gluon.WithVersionInfo( + int(version.Major()), + int(version.Minor()), + int(version.Patch()), + constants.FullAppName, + "TODO", + "TODO", + ), + gluon.WithLogger( + logrus.StandardLogger().WriterLevel(logrus.InfoLevel), + logrus.StandardLogger().WriterLevel(logrus.InfoLevel), + ), + ) + if err != nil { + return nil, err + } + + return imapServer, nil +} diff --git a/internal/bridge/locations.go b/internal/bridge/locations.go index 4c2bc597..c4c6b7dd 100644 --- a/internal/bridge/locations.go +++ b/internal/bridge/locations.go @@ -1,30 +1,13 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - package bridge -func (b *Bridge) ProvideLogsPath() (string, error) { - return b.locations.ProvideLogsPath() +func (bridge *Bridge) GetLogsPath() (string, error) { + return bridge.locator.ProvideLogsPath() } -func (b *Bridge) GetLicenseFilePath() string { - return b.locations.GetLicenseFilePath() +func (bridge *Bridge) GetLicenseFilePath() string { + return bridge.locator.GetLicenseFilePath() } -func (b *Bridge) GetDependencyLicensesLink() string { - return b.locations.GetDependencyLicensesLink() +func (bridge *Bridge) GetDependencyLicensesLink() string { + return bridge.locator.GetDependencyLicensesLink() } diff --git a/internal/bridge/mocks.go b/internal/bridge/mocks.go new file mode 100644 index 00000000..67fd3dc4 --- /dev/null +++ b/internal/bridge/mocks.go @@ -0,0 +1,127 @@ +package bridge + +import ( + "context" + "crypto/tls" + "errors" + "net" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/proton-bridge/v2/internal/bridge/mocks" + "github.com/ProtonMail/proton-bridge/v2/internal/updater" + "github.com/golang/mock/gomock" +) + +type Mocks struct { + TLSDialer *TestDialer + ProxyDialer *mocks.MockProxyDialer + + TLSReporter *mocks.MockTLSReporter + TLSIssueCh chan struct{} + + Updater *TestUpdater + Autostarter *mocks.MockAutostarter +} + +func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks { + ctl := gomock.NewController(tb) + + mocks := &Mocks{ + TLSDialer: NewTestDialer(), + ProxyDialer: mocks.NewMockProxyDialer(ctl), + + TLSReporter: mocks.NewMockTLSReporter(ctl), + TLSIssueCh: make(chan struct{}), + + Updater: NewTestUpdater(version, minAuto), + Autostarter: mocks.NewMockAutostarter(ctl), + } + + // When using the proxy dialer, we want to use the test dialer. + mocks.ProxyDialer.EXPECT().DialTLSContext( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).DoAndReturn(func(ctx context.Context, network, address string) (net.Conn, error) { + return mocks.TLSDialer.DialTLSContext(ctx, network, address) + }).AnyTimes() + + // When getting the TLS issue channel, we want to return the test channel. + mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes() + + return mocks +} + +type TestDialer struct { + canDial bool +} + +func NewTestDialer() *TestDialer { + return &TestDialer{ + canDial: true, + } +} + +func (d *TestDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) { + if !d.canDial { + return nil, errors.New("cannot dial") + } + + return (&tls.Dialer{Config: &tls.Config{InsecureSkipVerify: true}}).DialContext(ctx, network, address) +} + +func (d *TestDialer) SetCanDial(canDial bool) { + d.canDial = canDial +} + +type TestLocationsProvider struct { + config, cache string +} + +func NewTestLocationsProvider(tb testing.TB) *TestLocationsProvider { + return &TestLocationsProvider{ + config: tb.TempDir(), + cache: tb.TempDir(), + } +} + +func (provider *TestLocationsProvider) UserConfig() string { + return provider.config +} + +func (provider *TestLocationsProvider) UserCache() string { + return provider.cache +} + +type TestUpdater struct { + latest updater.VersionInfo +} + +func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater { + return &TestUpdater{ + latest: updater.VersionInfo{ + Version: version, + MinAuto: minAuto, + + RolloutProportion: 1.0, + }, + } +} + +func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Version) { + testUpdater.latest = updater.VersionInfo{ + Version: version, + MinAuto: minAuto, + + RolloutProportion: 1.0, + } +} + +func (updater *TestUpdater) GetVersionInfo(downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) { + return updater.latest, nil +} + +func (updater *TestUpdater) InstallUpdate(downloader updater.Downloader, update updater.VersionInfo) error { + return nil +} diff --git a/internal/bridge/mocks/mocks.go b/internal/bridge/mocks/mocks.go new file mode 100644 index 00000000..547a9bd2 --- /dev/null +++ b/internal/bridge/mocks/mocks.go @@ -0,0 +1,163 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ProtonMail/proton-bridge/v2/internal/bridge (interfaces: TLSReporter,ProxyDialer,Autostarter) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + net "net" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockTLSReporter is a mock of TLSReporter interface. +type MockTLSReporter struct { + ctrl *gomock.Controller + recorder *MockTLSReporterMockRecorder +} + +// MockTLSReporterMockRecorder is the mock recorder for MockTLSReporter. +type MockTLSReporterMockRecorder struct { + mock *MockTLSReporter +} + +// NewMockTLSReporter creates a new mock instance. +func NewMockTLSReporter(ctrl *gomock.Controller) *MockTLSReporter { + mock := &MockTLSReporter{ctrl: ctrl} + mock.recorder = &MockTLSReporterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTLSReporter) EXPECT() *MockTLSReporterMockRecorder { + return m.recorder +} + +// GetTLSIssueCh mocks base method. +func (m *MockTLSReporter) GetTLSIssueCh() <-chan struct{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTLSIssueCh") + ret0, _ := ret[0].(<-chan struct{}) + return ret0 +} + +// GetTLSIssueCh indicates an expected call of GetTLSIssueCh. +func (mr *MockTLSReporterMockRecorder) GetTLSIssueCh() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTLSIssueCh", reflect.TypeOf((*MockTLSReporter)(nil).GetTLSIssueCh)) +} + +// MockProxyDialer is a mock of ProxyDialer interface. +type MockProxyDialer struct { + ctrl *gomock.Controller + recorder *MockProxyDialerMockRecorder +} + +// MockProxyDialerMockRecorder is the mock recorder for MockProxyDialer. +type MockProxyDialerMockRecorder struct { + mock *MockProxyDialer +} + +// NewMockProxyDialer creates a new mock instance. +func NewMockProxyDialer(ctrl *gomock.Controller) *MockProxyDialer { + mock := &MockProxyDialer{ctrl: ctrl} + mock.recorder = &MockProxyDialerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProxyDialer) EXPECT() *MockProxyDialerMockRecorder { + return m.recorder +} + +// AllowProxy mocks base method. +func (m *MockProxyDialer) AllowProxy() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AllowProxy") +} + +// AllowProxy indicates an expected call of AllowProxy. +func (mr *MockProxyDialerMockRecorder) AllowProxy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowProxy", reflect.TypeOf((*MockProxyDialer)(nil).AllowProxy)) +} + +// DialTLSContext mocks base method. +func (m *MockProxyDialer) DialTLSContext(arg0 context.Context, arg1, arg2 string) (net.Conn, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DialTLSContext", arg0, arg1, arg2) + ret0, _ := ret[0].(net.Conn) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DialTLSContext indicates an expected call of DialTLSContext. +func (mr *MockProxyDialerMockRecorder) DialTLSContext(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialTLSContext", reflect.TypeOf((*MockProxyDialer)(nil).DialTLSContext), arg0, arg1, arg2) +} + +// DisallowProxy mocks base method. +func (m *MockProxyDialer) DisallowProxy() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DisallowProxy") +} + +// DisallowProxy indicates an expected call of DisallowProxy. +func (mr *MockProxyDialerMockRecorder) DisallowProxy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisallowProxy", reflect.TypeOf((*MockProxyDialer)(nil).DisallowProxy)) +} + +// MockAutostarter is a mock of Autostarter interface. +type MockAutostarter struct { + ctrl *gomock.Controller + recorder *MockAutostarterMockRecorder +} + +// MockAutostarterMockRecorder is the mock recorder for MockAutostarter. +type MockAutostarterMockRecorder struct { + mock *MockAutostarter +} + +// NewMockAutostarter creates a new mock instance. +func NewMockAutostarter(ctrl *gomock.Controller) *MockAutostarter { + mock := &MockAutostarter{ctrl: ctrl} + mock.recorder = &MockAutostarterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAutostarter) EXPECT() *MockAutostarterMockRecorder { + return m.recorder +} + +// Disable mocks base method. +func (m *MockAutostarter) Disable() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Disable") + ret0, _ := ret[0].(error) + return ret0 +} + +// Disable indicates an expected call of Disable. +func (mr *MockAutostarterMockRecorder) Disable() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disable", reflect.TypeOf((*MockAutostarter)(nil).Disable)) +} + +// Enable mocks base method. +func (m *MockAutostarter) Enable() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Enable") + ret0, _ := ret[0].(error) + return ret0 +} + +// Enable indicates an expected call of Enable. +func (mr *MockAutostarterMockRecorder) Enable() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockAutostarter)(nil).Enable)) +} diff --git a/internal/bridge/release_notes.go b/internal/bridge/release_notes.go deleted file mode 100644 index 117f8b3b..00000000 --- a/internal/bridge/release_notes.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Code generated by ./release-notes.sh at 'Fri Jan 22 11:01:06 AM CET 2021'. DO NOT EDIT. - -package bridge - -const ReleaseNotes = ` -` - -const ReleaseFixedBugs = `• Fixed sending error caused by inconsistent use of upper and lower case in sender’s email address -` diff --git a/internal/bridge/settings.go b/internal/bridge/settings.go index de1ec895..50d607df 100644 --- a/internal/bridge/settings.go +++ b/internal/bridge/settings.go @@ -1,44 +1,175 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - package bridge -import "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" +import ( + "context" -func (b *Bridge) Get(key settings.Key) string { - return b.settings.Get(key) + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/proton-bridge/v2/internal/updater" + "github.com/ProtonMail/proton-bridge/v2/internal/vault" +) + +func (bridge *Bridge) GetKeychainApp() (string, error) { + vaultDir, err := bridge.locator.ProvideSettingsPath() + if err != nil { + return "", err + } + + return vault.GetHelper(vaultDir) } -func (b *Bridge) Set(key settings.Key, value string) { - b.settings.Set(key, value) +func (bridge *Bridge) SetKeychainApp(helper string) error { + vaultDir, err := bridge.locator.ProvideSettingsPath() + if err != nil { + return err + } + + return vault.SetHelper(vaultDir, helper) } -func (b *Bridge) GetBool(key settings.Key) bool { - return b.settings.GetBool(key) +func (bridge *Bridge) GetGluonDir() string { + return bridge.vault.GetGluonDir() } -func (b *Bridge) SetBool(key settings.Key, value bool) { - b.settings.SetBool(key, value) +func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error { + if newGluonDir == bridge.GetGluonDir() { + return nil + } + + if err := bridge.closeIMAP(context.Background()); err != nil { + return err + } + + if err := moveDir(bridge.GetGluonDir(), newGluonDir); err != nil { + return err + } + + if err := bridge.vault.SetGluonDir(newGluonDir); err != nil { + return err + } + + imapServer, err := newIMAPServer(bridge.vault.GetGluonDir(), bridge.curVersion, bridge.tlsConfig) + if err != nil { + return err + } + + for _, user := range bridge.users { + imapConn, err := user.NewGluonConnector(ctx) + if err != nil { + return err + } + + if err := imapServer.LoadUser(context.Background(), imapConn, user.GluonID(), user.GluonKey()); err != nil { + return err + } + } + + bridge.imapServer = imapServer + + return bridge.serveIMAP() } -func (b *Bridge) GetInt(key settings.Key) int { - return b.settings.GetInt(key) +func (bridge *Bridge) GetProxyAllowed() bool { + return bridge.vault.GetProxyAllowed() } -func (b *Bridge) SetInt(key settings.Key, value int) { - b.settings.SetInt(key, value) +func (bridge *Bridge) SetProxyAllowed(allowed bool) error { + if allowed { + bridge.proxyDialer.AllowProxy() + } else { + bridge.proxyDialer.DisallowProxy() + } + + return bridge.vault.SetProxyAllowed(allowed) +} + +func (bridge *Bridge) GetShowAllMail() bool { + return bridge.vault.GetShowAllMail() +} + +func (bridge *Bridge) SetShowAllMail(show bool) error { + panic("TODO") +} + +func (bridge *Bridge) GetAutostart() bool { + return bridge.vault.GetAutostart() +} + +func (bridge *Bridge) SetAutostart(autostart bool) error { + if err := bridge.vault.SetAutostart(autostart); err != nil { + return err + } + + var err error + + if autostart { + err = bridge.autostarter.Enable() + } else { + err = bridge.autostarter.Disable() + } + + return err +} + +func (bridge *Bridge) GetAutoUpdate() bool { + return bridge.vault.GetAutoUpdate() +} + +func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error { + if bridge.vault.GetAutoUpdate() == autoUpdate { + return nil + } + + if err := bridge.vault.SetAutoUpdate(autoUpdate); err != nil { + return err + } + + bridge.updateCheckCh <- struct{}{} + + return nil +} + +func (bridge *Bridge) GetUpdateChannel() updater.Channel { + return updater.Channel(bridge.vault.GetUpdateChannel()) +} + +func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error { + if bridge.vault.GetUpdateChannel() == channel { + return nil + } + + if err := bridge.vault.SetUpdateChannel(channel); err != nil { + return err + } + + bridge.updateCheckCh <- struct{}{} + + return nil +} + +func (bridge *Bridge) GetLastVersion() *semver.Version { + return bridge.vault.GetLastVersion() +} + +func (bridge *Bridge) GetFirstStart() bool { + return bridge.vault.GetFirstStart() +} + +func (bridge *Bridge) SetFirstStart(firstStart bool) error { + return bridge.vault.SetFirstStart(firstStart) +} + +func (bridge *Bridge) GetFirstStartGUI() bool { + return bridge.vault.GetFirstStartGUI() +} + +func (bridge *Bridge) SetFirstStartGUI(firstStart bool) error { + return bridge.vault.SetFirstStartGUI(firstStart) +} + +func (bridge *Bridge) GetColorScheme() string { + return bridge.vault.GetColorScheme() +} + +func (bridge *Bridge) SetColorScheme(colorScheme string) error { + return bridge.vault.SetColorScheme(colorScheme) } diff --git a/internal/bridge/settings_test.go b/internal/bridge/settings_test.go new file mode 100644 index 00000000..2397b958 --- /dev/null +++ b/internal/bridge/settings_test.go @@ -0,0 +1,156 @@ +package bridge_test + +import ( + "context" + "os" + "testing" + + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" + "github.com/stretchr/testify/require" + "gitlab.protontech.ch/go/liteapi/server" +) + +func TestBridge_Settings_GluonDir(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Create a user. + _, err := bridge.LoginUser(context.Background(), username, password, nil, nil) + require.NoError(t, err) + + // Create a new location for the Gluon data. + newGluonDir := t.TempDir() + + // Move the gluon dir; it should also move the user's data. + require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir)) + + // Check that the new directory is not empty. + entries, err := os.ReadDir(newGluonDir) + require.NoError(t, err) + + // There should be at least one entry. + require.NotEmpty(t, entries) + }) + }) +} + +func TestBridge_Settings_IMAPPort(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // By default, the port is 1143. + require.Equal(t, 1143, bridge.GetIMAPPort()) + + // Set the port to 1144. + require.NoError(t, bridge.SetIMAPPort(1144)) + + // Get the new setting. + require.Equal(t, 1144, bridge.GetIMAPPort()) + }) + }) +} + +func TestBridge_Settings_IMAPSSL(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // By default, IMAP SSL is disabled. + require.False(t, bridge.GetIMAPSSL()) + + // Enable IMAP SSL. + require.NoError(t, bridge.SetIMAPSSL(true)) + + // Get the new setting. + require.True(t, bridge.GetIMAPSSL()) + }) + }) +} + +func TestBridge_Settings_SMTPPort(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // By default, the port is 1025. + require.Equal(t, 1025, bridge.GetSMTPPort()) + + // Set the port to 1024. + require.NoError(t, bridge.SetSMTPPort(1024)) + + // Get the new setting. + require.Equal(t, 1024, bridge.GetSMTPPort()) + }) + }) +} + +func TestBridge_Settings_SMTPSSL(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // By default, SMTP SSL is disabled. + require.False(t, bridge.GetSMTPSSL()) + + // Enable SMTP SSL. + require.NoError(t, bridge.SetSMTPSSL(true)) + + // Get the new setting. + require.True(t, bridge.GetSMTPSSL()) + }) + }) +} + +func TestBridge_Settings_Proxy(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // By default, proxy is allowed. + require.True(t, bridge.GetProxyAllowed()) + + // Disallow proxy. + mocks.ProxyDialer.EXPECT().DisallowProxy() + require.NoError(t, bridge.SetProxyAllowed(false)) + + // Get the new setting. + require.False(t, bridge.GetProxyAllowed()) + }) + }) +} + +func TestBridge_Settings_Autostart(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // By default, autostart is disabled. + require.False(t, bridge.GetAutostart()) + + // Enable autostart. + mocks.Autostarter.EXPECT().Enable().Return(nil) + require.NoError(t, bridge.SetAutostart(true)) + + // Get the new setting. + require.True(t, bridge.GetAutostart()) + }) + }) +} + +func TestBridge_Settings_FirstStart(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // By default, first start is true. + require.True(t, bridge.GetFirstStart()) + + // Set first start to false. + require.NoError(t, bridge.SetFirstStart(false)) + + // Get the new setting. + require.False(t, bridge.GetFirstStart()) + }) + }) +} + +func TestBridge_Settings_FirstStartGUI(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // By default, first start is true. + require.True(t, bridge.GetFirstStartGUI()) + + // Set first start to false. + require.NoError(t, bridge.SetFirstStartGUI(false)) + + // Get the new setting. + require.False(t, bridge.GetFirstStartGUI()) + }) + }) +} diff --git a/internal/bridge/smtp.go b/internal/bridge/smtp.go new file mode 100644 index 00000000..96feb974 --- /dev/null +++ b/internal/bridge/smtp.go @@ -0,0 +1,109 @@ +package bridge + +import ( + "crypto/tls" + "fmt" + + "github.com/ProtonMail/proton-bridge/v2/internal/constants" + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" + "github.com/sirupsen/logrus" +) + +func (bridge *Bridge) GetSMTPPort() int { + return bridge.vault.GetSMTPPort() +} + +func (bridge *Bridge) SetSMTPPort(newPort int) error { + if newPort == bridge.vault.GetSMTPPort() { + return nil + } + + if err := bridge.vault.SetSMTPPort(newPort); err != nil { + return err + } + + return bridge.restartSMTP() +} + +func (bridge *Bridge) GetSMTPSSL() bool { + return bridge.vault.GetSMTPSSL() +} + +func (bridge *Bridge) SetSMTPSSL(newSSL bool) error { + if newSSL == bridge.vault.GetSMTPSSL() { + return nil + } + + if err := bridge.vault.SetSMTPSSL(newSSL); err != nil { + return err + } + + return bridge.restartSMTP() +} + +func (bridge *Bridge) serveSMTP() error { + smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig) + if err != nil { + return fmt.Errorf("failed to create SMTP listener: %w", err) + } + + bridge.smtpListener = smtpListener + + go func() { + if err := bridge.smtpServer.Serve(bridge.smtpListener); err != nil { + logrus.WithError(err).Error("SMTP server stopped") + } + }() + + return nil +} + +func (bridge *Bridge) restartSMTP() error { + if err := bridge.closeSMTP(); err != nil { + return err + } + + smtpServer, err := newSMTPServer(bridge.smtpBackend, bridge.tlsConfig) + if err != nil { + return err + } + + bridge.smtpServer = smtpServer + + return bridge.serveSMTP() +} + +func (bridge *Bridge) closeSMTP() error { + if err := bridge.smtpServer.Close(); err != nil { + logrus.WithError(err).Warn("Failed to close SMTP server") + } + + // Don't close the SMTP listener -- it's closed by the server. + + return nil +} + +func newSMTPServer(smtpBackend *smtpBackend, tlsConfig *tls.Config) (*smtp.Server, error) { + smtpServer := smtp.NewServer(smtpBackend) + + smtpServer.TLSConfig = tlsConfig + smtpServer.Domain = constants.Host + smtpServer.AllowInsecureAuth = true + smtpServer.MaxLineLength = 1 << 16 + + smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server { + return sasl.NewLoginServer(func(address, password string) error { + user, err := conn.Server().Backend.Login(nil, address, password) + if err != nil { + return err + } + + conn.SetSession(user) + + return nil + }) + }) + + return smtpServer, nil +} diff --git a/internal/bridge/smtp_backend.go b/internal/bridge/smtp_backend.go new file mode 100644 index 00000000..cb08d686 --- /dev/null +++ b/internal/bridge/smtp_backend.go @@ -0,0 +1,70 @@ +package bridge + +import ( + "sync" + + "github.com/ProtonMail/proton-bridge/v2/internal/user" + "github.com/bradenaw/juniper/xslices" + "github.com/emersion/go-smtp" + "golang.org/x/exp/slices" +) + +type smtpBackend struct { + users []*user.User + usersLock sync.RWMutex +} + +func newSMTPBackend() (*smtpBackend, error) { + return &smtpBackend{}, nil +} + +func (backend *smtpBackend) Login(state *smtp.ConnectionState, username string, password string) (smtp.Session, error) { + backend.usersLock.RLock() + defer backend.usersLock.RUnlock() + + for _, user := range backend.users { + if slices.Contains(user.Addresses(), username) && user.BridgePass() == password { + return user.NewSMTPSession(username) + } + } + + return nil, ErrNoSuchUser +} + +func (backend *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { + return nil, ErrNotImplemented +} + +// addUser adds the given user to the backend. +// It returns an error if a user with the same ID already exists. +func (backend *smtpBackend) addUser(user *user.User) error { + backend.usersLock.Lock() + defer backend.usersLock.Unlock() + + for _, u := range backend.users { + if u.ID() == user.ID() { + return ErrUserAlreadyExists + } + } + + backend.users = append(backend.users, user) + + return nil +} + +// removeUser removes the given user from the backend. +// It returns an error if the user doesn't exist. +func (backend *smtpBackend) removeUser(user *user.User) error { + backend.usersLock.Lock() + defer backend.usersLock.Unlock() + + idx := xslices.Index(backend.users, user) + + if idx < 0 { + return ErrNoSuchUser + } + + backend.users = append(backend.users[:idx], backend.users[idx+1:]...) + + return nil +} diff --git a/internal/bridge/store_factory.go b/internal/bridge/store_factory.go deleted file mode 100644 index 4a5f7a0a..00000000 --- a/internal/bridge/store_factory.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package bridge - -import ( - "fmt" - "path/filepath" - - "github.com/ProtonMail/proton-bridge/v2/internal/sentry" - "github.com/ProtonMail/proton-bridge/v2/internal/store" - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" - "github.com/ProtonMail/proton-bridge/v2/internal/users" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" -) - -type storeFactory struct { - cacheProvider CacheProvider - sentryReporter *sentry.Reporter - panicHandler users.PanicHandler - eventListener listener.Listener - events *store.Events - cache cache.Cache - builder *message.Builder -} - -func newStoreFactory( - cacheProvider CacheProvider, - sentryReporter *sentry.Reporter, - panicHandler users.PanicHandler, - eventListener listener.Listener, - cache cache.Cache, - builder *message.Builder, -) *storeFactory { - return &storeFactory{ - cacheProvider: cacheProvider, - sentryReporter: sentryReporter, - panicHandler: panicHandler, - eventListener: eventListener, - events: store.NewEvents(cacheProvider.GetIMAPCachePath()), - cache: cache, - builder: builder, - } -} - -// New creates new store for given user. -func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) { - return store.New( - f.sentryReporter, - f.panicHandler, - user, - f.eventListener, - f.cache, - f.builder, - getUserStorePath(f.cacheProvider.GetDBDir(), user.ID()), - f.events, - ) -} - -// Remove removes all store files for given user. -func (f *storeFactory) Remove(userID string) error { - return store.RemoveStore( - f.events, - getUserStorePath(f.cacheProvider.GetDBDir(), userID), - userID, - ) -} - -// getUserStorePath returns the file path of the store database for the given userID. -func getUserStorePath(storeDir string, userID string) (path string) { - return filepath.Join(storeDir, fmt.Sprintf("mailbox-%v.db", userID)) -} diff --git a/internal/bridge/tls.go b/internal/bridge/tls.go index c652383a..ca5c1848 100644 --- a/internal/bridge/tls.go +++ b/internal/bridge/tls.go @@ -1,64 +1,5 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - package bridge -import ( - "crypto/tls" - - pkgTLS "github.com/ProtonMail/proton-bridge/v2/internal/config/tls" - "github.com/pkg/errors" - logrus "github.com/sirupsen/logrus" -) - -func (b *Bridge) GetTLSConfig() (*tls.Config, error) { - if !b.tls.HasCerts() { - if err := b.generateTLSCerts(); err != nil { - return nil, err - } - } - - tlsConfig, err := b.tls.GetConfig() - if err == nil { - return tlsConfig, nil - } - - logrus.WithError(err).Error("Failed to load TLS config, regenerating certificates") - - if err := b.generateTLSCerts(); err != nil { - return nil, err - } - - return b.tls.GetConfig() -} - -func (b *Bridge) generateTLSCerts() error { - template, err := pkgTLS.NewTLSTemplate() - if err != nil { - return errors.Wrap(err, "failed to generate TLS template") - } - - if err := b.tls.GenerateCerts(template); err != nil { - return errors.Wrap(err, "failed to generate TLS certs") - } - - if err := b.tls.InstallCerts(); err != nil { - return errors.Wrap(err, "failed to install TLS certs") - } - - return nil +func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) { + return bridge.vault.GetBridgeTLSCert(), bridge.vault.GetBridgeTLSKey() } diff --git a/internal/bridge/types.go b/internal/bridge/types.go index 028fbc71..55f1dd42 100644 --- a/internal/bridge/types.go +++ b/internal/bridge/types.go @@ -1,62 +1,43 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - package bridge import ( - "github.com/Masterminds/semver/v3" + "context" + "net" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" "github.com/ProtonMail/proton-bridge/v2/internal/updater" ) type Locator interface { + ProvideSettingsPath() (string, error) ProvideLogsPath() (string, error) - GetLicenseFilePath() string GetDependencyLicensesLink() string - - Clear() error - ClearUpdates() error } -type CacheProvider interface { - GetIMAPCachePath() string - GetDBDir() string - GetDefaultMessageCacheDir() string +type Identifier interface { + GetUserAgent() string + HasClient() bool + SetClient(name, version string) + SetPlatform(platform string) } -type SettingsProvider interface { - Get(key settings.Key) string - Set(key settings.Key, value string) +type TLSReporter interface { + GetTLSIssueCh() <-chan struct{} +} - GetBool(key settings.Key) bool - SetBool(key settings.Key, val bool) +type ProxyDialer interface { + DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) - GetInt(key settings.Key) int - SetInt(key settings.Key, val int) + AllowProxy() + DisallowProxy() +} + +type Autostarter interface { + Enable() error + Disable() error } type Updater interface { - Check() (updater.VersionInfo, error) - IsDowngrade(updater.VersionInfo) bool - InstallUpdate(updater.VersionInfo) error -} - -type Versioner interface { - RemoveOtherVersions(*semver.Version) error + GetVersionInfo(downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) + InstallUpdate(downloader updater.Downloader, update updater.VersionInfo) error } diff --git a/internal/bridge/updates.go b/internal/bridge/updates.go new file mode 100644 index 00000000..85e43ca1 --- /dev/null +++ b/internal/bridge/updates.go @@ -0,0 +1,72 @@ +package bridge + +import ( + "time" + + "github.com/ProtonMail/proton-bridge/v2/internal/constants" + "github.com/ProtonMail/proton-bridge/v2/internal/events" + "github.com/ProtonMail/proton-bridge/v2/internal/updater" +) + +func (bridge *Bridge) CheckForUpdates() { + bridge.updateCheckCh <- struct{}{} +} + +func (bridge *Bridge) watchForUpdates() error { + ticker := time.NewTicker(constants.UpdateCheckInterval) + + go func() { + for { + select { + case <-bridge.updateCheckCh: + case <-ticker.C: + } + + version, err := bridge.updater.GetVersionInfo(bridge.api, bridge.vault.GetUpdateChannel()) + if err != nil { + continue + } + + if err := bridge.handleUpdate(version); err != nil { + continue + } + } + }() + + bridge.updateCheckCh <- struct{}{} + + return nil +} + +func (bridge *Bridge) handleUpdate(version updater.VersionInfo) error { + switch { + case !version.Version.GreaterThan(bridge.curVersion): + bridge.publish(events.UpdateNotAvailable{}) + + case version.RolloutProportion < bridge.vault.GetUpdateRollout(): + bridge.publish(events.UpdateNotAvailable{}) + + case bridge.curVersion.LessThan(version.MinAuto): + bridge.publish(events.UpdateAvailable{ + Version: version, + CanInstall: false, + }) + + case !bridge.vault.GetAutoUpdate(): + bridge.publish(events.UpdateAvailable{ + Version: version, + CanInstall: true, + }) + + default: + if err := bridge.updater.InstallUpdate(bridge.api, version); err != nil { + return err + } + + bridge.publish(events.UpdateInstalled{ + Version: version, + }) + } + + return nil +} diff --git a/internal/bridge/useragent.go b/internal/bridge/useragent.go index f9368b2f..1b151122 100644 --- a/internal/bridge/useragent.go +++ b/internal/bridge/useragent.go @@ -1,26 +1,9 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - package bridge -func (b *Bridge) GetCurrentUserAgent() string { - return b.userAgent.String() +func (bridge *Bridge) GetCurrentUserAgent() string { + return bridge.identifier.GetUserAgent() } -func (b *Bridge) SetCurrentPlatform(platform string) { - b.userAgent.SetPlatform(platform) +func (bridge *Bridge) SetCurrentPlatform(platform string) { + bridge.identifier.SetPlatform(platform) } diff --git a/internal/bridge/users.go b/internal/bridge/users.go new file mode 100644 index 00000000..3a2667e4 --- /dev/null +++ b/internal/bridge/users.go @@ -0,0 +1,434 @@ +package bridge + +import ( + "context" + "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" + "github.com/go-resty/resty/v2" + "github.com/sirupsen/logrus" + "gitlab.protontech.ch/go/liteapi" + "golang.org/x/exp/slices" +) + +type UserInfo struct { + // UserID is the user's API ID. + UserID string + + // Username is the user's API username. + Username string + + // Connected is true if the user is logged in (has API auth). + Connected bool + + // Addresses holds the user's email addresses. The first address is the primary address. + Addresses []string + + // AddressMode is the user's address mode. + AddressMode AddressMode + + // BridgePass is the user's bridge password. + BridgePass string + + // UsedSpace is the amount of space used by the user. + UsedSpace int + + // MaxSpace is the total amount of space available to the user. + MaxSpace int +} + +type AddressMode int + +const ( + SplitMode AddressMode = iota + CombinedMode +) + +// GetUserIDs returns the IDs of all known users (authorized or not). +func (bridge *Bridge) GetUserIDs() []string { + return bridge.vault.GetUserIDs() +} + +// GetUserInfo returns info about the given user. +func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) { + vaultUser, err := bridge.vault.GetUser(userID) + if err != nil { + return UserInfo{}, err + } + + user, ok := bridge.users[userID] + if !ok { + return getUserInfo(vaultUser.UserID(), vaultUser.Username()), nil + } + + return getConnUserInfo(user), nil +} + +// QueryUserInfo queries the user info by username or address. +func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) { + for userID, user := range bridge.users { + if user.Match(query) { + return bridge.GetUserInfo(userID) + } + } + + return UserInfo{}, ErrNoSuchUser +} + +// LoginUser authorizes a new bridge user with the given username and password. +// If necessary, a TOTP and mailbox password are requested via the callbacks. +func (bridge *Bridge) LoginUser( + ctx context.Context, + username, password string, + getTOTP func() (string, error), + getKeyPass func() ([]byte, error), +) (string, error) { + client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password) + if err != nil { + return "", err + } + + if auth.TwoFA.Enabled == liteapi.TOTPEnabled { + totp, err := getTOTP() + if err != nil { + return "", err + } + + if err := client.Auth2FA(ctx, liteapi.Auth2FAReq{TwoFactorCode: totp}); err != nil { + return "", err + } + } + + var keyPass []byte + + if auth.PasswordMode == liteapi.TwoPasswordMode { + pass, err := getKeyPass() + if err != nil { + return "", err + } + + keyPass = pass + } else { + keyPass = []byte(password) + } + + apiUser, apiAddrs, userKR, addrKRs, saltedKeyPass, err := client.Unlock(ctx, keyPass) + if err != nil { + return "", err + } + + if err := bridge.addUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, auth.UID, auth.RefreshToken, saltedKeyPass); err != nil { + return "", err + } + + return apiUser.ID, nil +} + +// LogoutUser logs out the given user. +func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error { + return bridge.logoutUser(ctx, userID, true, false) +} + +// DeleteUser deletes the given user. +// If it is authorized, it is logged out first. +func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error { + if bridge.users[userID] != nil { + if err := bridge.logoutUser(ctx, userID, true, true); err != nil { + return err + } + } + + if err := bridge.vault.DeleteUser(userID); err != nil { + return err + } + + bridge.publish(events.UserDeleted{ + UserID: userID, + }) + + return nil +} + +func (bridge *Bridge) GetAddressMode(userID string) (AddressMode, error) { + panic("TODO") +} + +func (bridge *Bridge) SetAddressMode(userID string, mode AddressMode) error { + panic("TODO") +} + +// loadUsers loads authorized users from the vault. +func (bridge *Bridge) loadUsers(ctx context.Context) error { + for _, userID := range bridge.vault.GetUserIDs() { + user, err := bridge.vault.GetUser(userID) + if err != nil { + return err + } + + if user.AuthUID() == "" { + continue + } + + if err := bridge.loadUser(ctx, user); err != nil { + logrus.WithError(err).Error("Failed to load connected user") + + if err := user.Clear(); err != nil { + logrus.WithError(err).Error("Failed to clear user") + } + + continue + } + } + + return nil +} + +func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error { + client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef()) + if err != nil { + return fmt.Errorf("failed to create API client: %w", err) + } + + apiUser, apiAddrs, userKR, addrKRs, err := client.UnlockSalted(ctx, user.KeyPass()) + if err != nil { + return fmt.Errorf("failed to unlock user: %w", err) + } + + if err := bridge.addUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, auth.UID, auth.RefreshToken, user.KeyPass()); err != nil { + return fmt.Errorf("failed to add user: %w", err) + } + + bridge.publish(events.UserLoggedIn{ + UserID: user.UserID(), + }) + + return nil +} + +// addUser adds a new user with an already salted mailbox password. +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) + if err != nil { + return err + } + + user = existingUser + } else { + newUser, err := bridge.addNewUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, authUID, authRef, saltedKeyPass) + if err != nil { + return err + } + + user = newUser + } + + go func() { + for event := range user.GetNotifyCh() { + switch event := event.(type) { + case events.UserDeauth: + if err := bridge.logoutUser(context.Background(), event.UserID, false, false); err != nil { + logrus.WithError(err).Error("Failed to logout user") + } + } + + bridge.publish(event) + } + }() + + // Gluon will set the IMAP ID in the context, if known, before making requests on behalf of this user. + client.AddPreRequestHook(func(ctx context.Context, req *resty.Request) error { + if imapID, ok := imap.GetIMAPIDFromContext(ctx); ok { + bridge.identifier.SetClient(imapID.Name, imapID.Version) + } + + return nil + }) + + bridge.publish(events.UserLoggedIn{ + UserID: user.ID(), + }) + + return nil +} + +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) { + vaultUser, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, authUID, authRef, saltedKeyPass) + if err != nil { + return nil, err + } + + user, err := user.New(ctx, vaultUser, client, apiUser, apiAddrs, userKR, addrKRs) + if err != nil { + return nil, err + } + + gluonKey, err := crypto.RandomToken(32) + if err != nil { + return nil, err + } + + imapConn, err := user.NewGluonConnector(ctx) + if err != nil { + return nil, err + } + + gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, gluonKey) + if err != nil { + return nil, err + } + + if err := vaultUser.UpdateGluonData(gluonID, gluonKey); err != nil { + return nil, err + } + + if err := bridge.smtpBackend.addUser(user); err != nil { + return nil, err + } + + bridge.users[apiUser.ID] = user + + return user, nil +} + +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) { + vaultUser, err := bridge.vault.GetUser(apiUser.ID) + if err != nil { + return nil, err + } + + if err := vaultUser.UpdateAuth(authUID, authRef); err != nil { + return nil, err + } + + if err := vaultUser.UpdateKeyPass(saltedKeyPass); err != nil { + return nil, err + } + + user, err := user.New(ctx, vaultUser, client, apiUser, apiAddrs, userKR, addrKRs) + if err != nil { + return nil, err + } + + imapConn, err := user.NewGluonConnector(ctx) + if err != nil { + return nil, err + } + + if err := bridge.imapServer.LoadUser(ctx, imapConn, user.GluonID(), user.GluonKey()); err != nil { + return nil, err + } + + if err := bridge.smtpBackend.addUser(user); err != nil { + return nil, err + } + + bridge.users[apiUser.ID] = user + + return user, nil +} + +// logoutUser closes and removes the user with the given ID. +// If withAPI is true, the user will additionally be logged out from API. +// If withFiles is true, the user's files will be deleted. +func (bridge *Bridge) logoutUser(ctx context.Context, userID string, withAPI, withFiles bool) error { + user, ok := bridge.users[userID] + if !ok { + return ErrNoSuchUser + } + + vaultUser, err := bridge.vault.GetUser(userID) + if err != nil { + return err + } + + if err := bridge.imapServer.RemoveUser(ctx, vaultUser.GluonID(), withFiles); err != nil { + return err + } + + if err := bridge.smtpBackend.removeUser(user); err != nil { + return err + } + + if withAPI { + if err := user.Logout(ctx); err != nil { + return err + } + } + + if err := user.Close(ctx); err != nil { + return err + } + + if err := vaultUser.Clear(); err != nil { + return err + } + + delete(bridge.users, userID) + + bridge.publish(events.UserLoggedOut{ + UserID: userID, + }) + + return nil +} + +// getUserInfo returns information about a disconnected user. +func getUserInfo(userID, username string) UserInfo { + return UserInfo{ + UserID: userID, + Username: username, + AddressMode: CombinedMode, + } +} + +// getConnUserInfo returns information about a connected user. +func getConnUserInfo(user *user.User) UserInfo { + return UserInfo{ + Connected: true, + UserID: user.ID(), + Username: user.Name(), + Addresses: user.Addresses(), + AddressMode: CombinedMode, + BridgePass: user.BridgePass(), + UsedSpace: user.UsedSpace(), + MaxSpace: user.MaxSpace(), + } +} diff --git a/internal/bridge/users_test.go b/internal/bridge/users_test.go new file mode 100644 index 00000000..51dd838b --- /dev/null +++ b/internal/bridge/users_test.go @@ -0,0 +1,286 @@ +package bridge_test + +import ( + "context" + "testing" + "time" + + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" + "github.com/ProtonMail/proton-bridge/v2/internal/events" + "github.com/stretchr/testify/require" + "gitlab.protontech.ch/go/liteapi/server" +) + +func TestBridge_WithoutUsers(t *testing.T) { + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + require.Empty(t, bridge.GetUserIDs()) + require.Empty(t, getConnectedUserIDs(t, bridge)) + }) + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + require.Empty(t, bridge.GetUserIDs()) + require.Empty(t, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_Login(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID, err := bridge.LoginUser(ctx, username, password, nil, nil) + require.NoError(t, err) + + // The user is now connected. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_LoginLogoutLogin(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID := must(bridge.LoginUser(ctx, username, password, nil, nil)) + + // The user is now connected. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) + + // Logout the user. + require.NoError(t, bridge.LogoutUser(ctx, userID)) + + // The user is now disconnected. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Empty(t, getConnectedUserIDs(t, bridge)) + + // Login the user again. + newUserID := must(bridge.LoginUser(ctx, username, password, nil, nil)) + require.Equal(t, userID, newUserID) + + // The user is connected again. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_LoginDeleteLogin(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID := must(bridge.LoginUser(ctx, username, password, nil, nil)) + + // The user is now connected. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) + + // Delete the user. + require.NoError(t, bridge.DeleteUser(ctx, userID)) + + // The user is now gone. + require.Empty(t, bridge.GetUserIDs()) + require.Empty(t, getConnectedUserIDs(t, bridge)) + + // Login the user again. + newUserID := must(bridge.LoginUser(ctx, username, password, nil, nil)) + require.Equal(t, userID, newUserID) + + // The user is connected again. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_LoginDeauthLogin(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID := must(bridge.LoginUser(ctx, username, password, nil, nil)) + + // Get a channel to receive the deauth event. + eventCh, done := bridge.GetEvents(events.UserDeauth{}) + defer done() + + // Deauth the user. + require.NoError(t, s.RevokeUser(userID)) + + // The user is eventually disconnected. + require.Eventually(t, func() bool { + return len(getConnectedUserIDs(t, bridge)) == 0 + }, 10*time.Second, time.Second) + + // We should get a deauth event. + require.IsType(t, events.UserDeauth{}, <-eventCh) + + // Login the user after the disconnection. + newUserID := must(bridge.LoginUser(ctx, username, password, nil, nil)) + require.Equal(t, userID, newUserID) + + // The user is connected again. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_LoginExpireLogin(t *testing.T) { + const authLife = 2 * time.Second + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + s.SetAuthLife(authLife) + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. Its auth will only be valid for a short time. + userID := must(bridge.LoginUser(ctx, username, password, nil, nil)) + + // Wait until the auth expires. + time.Sleep(authLife) + + // The user will have to refresh but the logout will still succeed. + require.NoError(t, bridge.LogoutUser(ctx, userID)) + }) + }) +} + +func TestBridge_FailToLoad(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + var userID string + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID = must(bridge.LoginUser(ctx, username, password, nil, nil)) + }) + + // Deauth the user while bridge is stopped. + require.NoError(t, s.RevokeUser(userID)) + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // The user is disconnected. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Empty(t, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_LoginRestart(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + var userID string + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID = must(bridge.LoginUser(ctx, username, password, nil, nil)) + }) + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // The user is still connected. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_LoginLogoutRestart(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + var userID string + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID = must(bridge.LoginUser(ctx, username, password, nil, nil)) + + // Logout the user. + require.NoError(t, bridge.LogoutUser(ctx, userID)) + }) + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // The user is still disconnected. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Empty(t, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_LoginDeleteRestart(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + var userID string + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID = must(bridge.LoginUser(ctx, username, password, nil, nil)) + + // Delete the user. + require.NoError(t, bridge.DeleteUser(ctx, userID)) + }) + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // The user is still gone. + require.Empty(t, bridge.GetUserIDs()) + require.Empty(t, getConnectedUserIDs(t, bridge)) + }) + }) +} + +func TestBridge_BridgePass(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + withEnv(t, func(s *server.Server, locator bridge.Locator, storeKey []byte) { + var userID, pass string + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // Login the user. + userID = must(bridge.LoginUser(ctx, username, password, nil, nil)) + + // Retrieve the bridge pass. + pass = must(bridge.GetUserInfo(userID)).BridgePass + + // Log the user out. + require.NoError(t, bridge.LogoutUser(ctx, userID)) + + // Log the user back in. + must(bridge.LoginUser(ctx, username, password, nil, nil)) + + // The bridge pass should be the same. + require.Equal(t, pass, pass) + }) + + withBridge(t, s.GetHostURL(), locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + // The bridge should load schizofrenic. + require.Equal(t, []string{userID}, bridge.GetUserIDs()) + require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) + + // The bridge pass should be the same. + require.Equal(t, pass, must(bridge.GetUserInfo(userID)).BridgePass) + }) + }) +} diff --git a/internal/config/tls/cert_store_darwin.go b/internal/certs/cert_store_darwin.go similarity index 64% rename from internal/config/tls/cert_store_darwin.go rename to internal/certs/cert_store_darwin.go index 847d9fb8..f90b841b 100644 --- a/internal/config/tls/cert_store_darwin.go +++ b/internal/certs/cert_store_darwin.go @@ -15,9 +15,31 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package tls +package certs -import "golang.org/x/sys/execabs" +import ( + "os" + + "golang.org/x/sys/execabs" +) + +func installCert(certPEM []byte) error { + name, err := writeToTempFile(certPEM) + if err != nil { + return err + } + + return addTrustedCert(name) +} + +func uninstallCert(certPEM []byte) error { + name, err := writeToTempFile(certPEM) + if err != nil { + return err + } + + return removeTrustedCert(name) +} func addTrustedCert(certPath string) error { return execabs.Command( //nolint:gosec @@ -44,10 +66,20 @@ func removeTrustedCert(certPath string) error { ).Run() } -func (t *TLS) InstallCerts() error { - return addTrustedCert(t.getTLSCertPath()) -} +// writeToTempFile writes the given data to a temporary file and returns the path. +func writeToTempFile(data []byte) (string, error) { + f, err := os.CreateTemp("", "tls") + if err != nil { + return "", err + } -func (t *TLS) UninstallCerts() error { - return removeTrustedCert(t.getTLSCertPath()) + if _, err := f.Write(data); err != nil { + return "", err + } + + if err := f.Close(); err != nil { + return "", err + } + + return f.Name(), nil } diff --git a/internal/config/tls/cert_store_linux.go b/internal/certs/cert_store_linux.go similarity index 90% rename from internal/config/tls/cert_store_linux.go rename to internal/certs/cert_store_linux.go index 614ad31a..c035c4e6 100644 --- a/internal/config/tls/cert_store_linux.go +++ b/internal/certs/cert_store_linux.go @@ -15,12 +15,12 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package tls +package certs -func (t *TLS) InstallCerts() error { +func installCert([]byte) error { return nil // Linux doesn't have a root cert store. } -func (t *TLS) UninstallCerts() error { +func uninstallCert([]byte) error { return nil // Linux doesn't have a root cert store. } diff --git a/internal/config/tls/cert_store_windows.go b/internal/certs/cert_store_windows.go similarity index 90% rename from internal/config/tls/cert_store_windows.go rename to internal/certs/cert_store_windows.go index d8af6749..1a7e1f24 100644 --- a/internal/config/tls/cert_store_windows.go +++ b/internal/certs/cert_store_windows.go @@ -15,12 +15,12 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package tls +package certs -func (t *TLS) InstallCerts() error { +func installCert([]byte) error { return nil // NOTE(GODT-986): Install certs to root cert store? } -func (t *TLS) UninstallCerts() error { +func uninstallCert([]byte) error { return nil // NOTE(GODT-986): Uninstall certs from root cert store? } diff --git a/internal/certs/installer.go b/internal/certs/installer.go new file mode 100644 index 00000000..d9ef9315 --- /dev/null +++ b/internal/certs/installer.go @@ -0,0 +1,15 @@ +package certs + +type Installer struct{} + +func NewInstaller() *Installer { + return &Installer{} +} + +func (installer *Installer) InstallCert(certPEM []byte) error { + return installCert(certPEM) +} + +func (installer *Installer) UninstallCert(certPEM []byte) error { + return uninstallCert(certPEM) +} diff --git a/internal/config/tls/tls.go b/internal/certs/tls.go similarity index 53% rename from internal/config/tls/tls.go rename to internal/certs/tls.go index ea10f11e..66e52a93 100644 --- a/internal/config/tls/tls.go +++ b/internal/certs/tls.go @@ -15,9 +15,10 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package tls +package certs import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/tls" @@ -27,22 +28,13 @@ import ( "fmt" "math/big" "net" - "os" - "path/filepath" "time" "github.com/pkg/errors" ) -type TLS struct { - settingsPath string -} - -func New(settingsPath string) *TLS { - return &TLS{ - settingsPath: settingsPath, - } -} +// ErrTLSCertExpiresSoon is returned when the TLS certificate is about to expire. +var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon") // NewTLSTemplate creates a new TLS template certificate with a random serial number. func NewTLSTemplate() (*x509.Certificate, error) { @@ -69,108 +61,40 @@ func NewTLSTemplate() (*x509.Certificate, error) { }, nil } -// NewPEMKeyPair return a new TLS private key and certificate in PEM encoded format. -func NewPEMKeyPair() (pemCert, pemKey []byte, err error) { - template, err := NewTLSTemplate() - if err != nil { - return nil, nil, errors.Wrap(err, "failed to generate TLS template") - } - +// GenerateTLSCert generates a new TLS certificate and returns it as PEM. +var GenerateCert = func(template *x509.Certificate) ([]byte, []byte, error) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, errors.Wrap(err, "failed to generate private key") } - pemKey = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) - derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) if err != nil { return nil, nil, errors.Wrap(err, "failed to create certificate") } - pemCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certPEM := new(bytes.Buffer) - return pemCert, pemKey, nil -} - -var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon") - -// getTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP). -func (t *TLS) getTLSCertPath() string { - return filepath.Join(t.settingsPath, "cert.pem") -} - -// getTLSKeyPath returns path to private key; used for TLS servers (IMAP, SMTP). -func (t *TLS) getTLSKeyPath() string { - return filepath.Join(t.settingsPath, "key.pem") -} - -// HasCerts returns whether TLS certs have been generated. -func (t *TLS) HasCerts() bool { - if _, err := os.Stat(t.getTLSCertPath()); err != nil { - return false + if err := pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + return nil, nil, err } - if _, err := os.Stat(t.getTLSKeyPath()); err != nil { - return false + keyPEM := new(bytes.Buffer) + + if err := pem.Encode(keyPEM, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { + return nil, nil, err } - return true -} - -// GenerateCerts generates certs from the given template. -func (t *TLS) GenerateCerts(template *x509.Certificate) error { - priv, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return errors.Wrap(err, "failed to generate private key") - } - - derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) - if err != nil { - return errors.Wrap(err, "failed to create certificate") - } - - certOut, err := os.Create(t.getTLSCertPath()) - if err != nil { - return err - } - defer certOut.Close() //nolint:errcheck,gosec - - if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { - return err - } - - keyOut, err := os.OpenFile(t.getTLSKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) - if err != nil { - return err - } - defer keyOut.Close() //nolint:errcheck,gosec - - return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + return certPEM.Bytes(), keyPEM.Bytes(), nil } // GetConfig tries to load TLS config or generate new one which is then returned. -func (t *TLS) GetConfig() (*tls.Config, error) { - c, err := tls.LoadX509KeyPair(t.getTLSCertPath(), t.getTLSKeyPath()) +func GetConfig(certPEM, keyPEM []byte) (*tls.Config, error) { + c, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { return nil, errors.Wrap(err, "failed to load keypair") } - return getConfigFromKeyPair(c) -} - -// GetConfigFromPEMKeyPair load a TLS config from PEM encoded certificate and key. -func GetConfigFromPEMKeyPair(permCert, pemKey []byte) (*tls.Config, error) { - c, err := tls.X509KeyPair(permCert, pemKey) - if err != nil { - return nil, errors.Wrap(err, "failed to load keypair") - } - - return getConfigFromKeyPair(c) -} - -func getConfigFromKeyPair(c tls.Certificate) (*tls.Config, error) { - var err error c.Leaf, err = x509.ParseCertificate(c.Certificate[0]) if err != nil { return nil, errors.Wrap(err, "failed to parse certificate") diff --git a/internal/config/tls/tls_test.go b/internal/certs/tls_test.go similarity index 82% rename from internal/config/tls/tls_test.go rename to internal/certs/tls_test.go index 83e84975..25d7409d 100644 --- a/internal/config/tls/tls_test.go +++ b/internal/certs/tls_test.go @@ -15,10 +15,10 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package tls +package certs import ( - "os" + "crypto/tls" "testing" "time" @@ -26,12 +26,6 @@ import ( ) func TestGetOldConfig(t *testing.T) { - dir, err := os.MkdirTemp("", "test-tls") - require.NoError(t, err) - - // Create new tls object. - tls := New(dir) - // Create new TLS template. tlsTemplate, err := NewTLSTemplate() require.NoError(t, err) @@ -41,20 +35,15 @@ func TestGetOldConfig(t *testing.T) { tlsTemplate.NotAfter = time.Now() // Generate the certs from the template. - require.NoError(t, tls.GenerateCerts(tlsTemplate)) + certPEM, keyPEM, err := GenerateCert(tlsTemplate) + require.NoError(t, err) // Generate the config from the certs -- it's going to expire soon so we don't want to use it. - _, err = tls.GetConfig() + _, err = GetConfig(certPEM, keyPEM) require.Equal(t, err, ErrTLSCertExpiresSoon) } func TestGetValidConfig(t *testing.T) { - dir, err := os.MkdirTemp("", "test-tls") - require.NoError(t, err) - - // Create new tls object. - tls := New(dir) - // Create new TLS template. tlsTemplate, err := NewTLSTemplate() require.NoError(t, err) @@ -64,10 +53,11 @@ func TestGetValidConfig(t *testing.T) { tlsTemplate.NotAfter = time.Now().Add(2 * 365 * 24 * time.Hour) // Generate the certs from the template. - require.NoError(t, tls.GenerateCerts(tlsTemplate)) + certPEM, keyPEM, err := GenerateCert(tlsTemplate) + require.NoError(t, err) // Generate the config from the certs -- it's not going to expire soon so we want to use it. - config, err := tls.GetConfig() + config, err := GetConfig(certPEM, keyPEM) require.NoError(t, err) require.Equal(t, len(config.Certificates), 1) @@ -77,9 +67,13 @@ func TestGetValidConfig(t *testing.T) { } func TestNewConfig(t *testing.T) { - pemCert, pemKey, err := NewPEMKeyPair() + tlsTemplate, err := NewTLSTemplate() require.NoError(t, err) - _, err = GetConfigFromPEMKeyPair(pemCert, pemKey) + pemCert, pemKey, err := GenerateCert(tlsTemplate) require.NoError(t, err) + + cert, err := tls.X509KeyPair(pemCert, pemKey) + require.NoError(t, err) + require.NotNil(t, cert) } diff --git a/internal/clientconfig/applemail.go b/internal/clientconfig/applemail.go index 8345767f..1bf847d3 100644 --- a/internal/clientconfig/applemail.go +++ b/internal/clientconfig/applemail.go @@ -23,7 +23,7 @@ import ( "strconv" "time" - "github.com/ProtonMail/proton-bridge/v2/internal/config/useragent" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" "github.com/ProtonMail/proton-bridge/v2/pkg/mobileconfig" "golang.org/x/sys/execabs" ) diff --git a/internal/config/cache/cache.go b/internal/config/cache/cache.go deleted file mode 100644 index 5fa3ff3c..00000000 --- a/internal/config/cache/cache.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package cache provides access to contents inside a cache directory. -package cache - -import ( - "os" - "path/filepath" - - "github.com/ProtonMail/proton-bridge/v2/pkg/files" -) - -type Cache struct { - dir, version string -} - -func New(dir, version string) (*Cache, error) { - if err := os.MkdirAll(filepath.Join(dir, version), 0o700); err != nil { - return nil, err - } - - return &Cache{ - dir: dir, - version: version, - }, nil -} - -// GetDBDir returns folder for db files. -func (c *Cache) GetDBDir() string { - return c.getCurrentCacheDir() -} - -// GetDefaultMessageCacheDir returns folder for cached messages files. -func (c *Cache) GetDefaultMessageCacheDir() string { - return filepath.Join(c.getCurrentCacheDir(), "messages") -} - -// GetIMAPCachePath returns path to file with IMAP status. -func (c *Cache) GetIMAPCachePath() string { - return filepath.Join(c.getCurrentCacheDir(), "user_info.json") -} - -// GetTransferDir returns folder for import-export rules files. -func (c *Cache) GetTransferDir() string { - return c.getCurrentCacheDir() -} - -// RemoveOldVersions removes any cache dirs that are not the current version. -func (c *Cache) RemoveOldVersions() error { - return files.Remove(c.dir).Except(c.getCurrentCacheDir()).Do() -} - -func (c *Cache) getCurrentCacheDir() string { - return filepath.Join(c.dir, c.version) -} diff --git a/internal/config/cache/cache_test.go b/internal/config/cache/cache_test.go deleted file mode 100644 index c8ce44b0..00000000 --- a/internal/config/cache/cache_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestRemoveOldVersions(t *testing.T) { - dir, err := os.MkdirTemp("", "test-cache") - require.NoError(t, err) - - cache, err := New(dir, "c4") - require.NoError(t, err) - - createFilesInDir(t, dir, - "unexpected1.txt", - "c1/unexpected1.txt", - "c2/unexpected2.txt", - "c3/unexpected3.txt", - "something.txt", - ) - - require.DirExists(t, filepath.Join(dir, "c4")) - require.FileExists(t, filepath.Join(dir, "unexpected1.txt")) - require.FileExists(t, filepath.Join(dir, "c1", "unexpected1.txt")) - require.FileExists(t, filepath.Join(dir, "c2", "unexpected2.txt")) - require.FileExists(t, filepath.Join(dir, "c3", "unexpected3.txt")) - require.FileExists(t, filepath.Join(dir, "something.txt")) - - assert.NoError(t, cache.RemoveOldVersions()) - - assert.DirExists(t, filepath.Join(dir, "c4")) - assert.NoFileExists(t, filepath.Join(dir, "unexpected1.txt")) - assert.NoFileExists(t, filepath.Join(dir, "c1", "unexpected1.txt")) - assert.NoFileExists(t, filepath.Join(dir, "c2", "unexpected2.txt")) - assert.NoFileExists(t, filepath.Join(dir, "c3", "unexpected3.txt")) - assert.NoFileExists(t, filepath.Join(dir, "something.txt")) -} - -func createFilesInDir(t *testing.T, dir string, files ...string) { - for _, target := range files { - require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, target)), 0o700)) - - f, err := os.Create(filepath.Join(dir, target)) - require.NoError(t, err) - require.NoError(t, f.Close()) - } -} diff --git a/internal/config/settings/kvs.go b/internal/config/settings/kvs.go deleted file mode 100644 index fedbf0a4..00000000 --- a/internal/config/settings/kvs.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package settings - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "strconv" - "sync" - - "github.com/sirupsen/logrus" -) - -type keyValueStore struct { - vals map[Key]string - path string - lock *sync.RWMutex -} - -// newKeyValueStore returns loaded preferences. -func newKeyValueStore(path string) *keyValueStore { - p := &keyValueStore{ - path: path, - lock: &sync.RWMutex{}, - } - if err := p.load(); err != nil { - logrus.WithError(err).Warn("Cannot load preferences file, creating new one") - } - return p -} - -func (p *keyValueStore) load() error { - if p.vals != nil { - return nil - } - - p.lock.Lock() - defer p.lock.Unlock() - - p.vals = make(map[Key]string) - - f, err := os.Open(p.path) - if err != nil { - return err - } - defer f.Close() //nolint:errcheck,gosec - - return json.NewDecoder(f).Decode(&p.vals) -} - -func (p *keyValueStore) save() error { - if p.vals == nil { - return errors.New("cannot save preferences: cache is nil") - } - - p.lock.Lock() - defer p.lock.Unlock() - - b, err := json.MarshalIndent(p.vals, "", "\t") - if err != nil { - return err - } - - return os.WriteFile(p.path, b, 0o600) -} - -func (p *keyValueStore) setDefault(key Key, value string) { - if p.Get(key) == "" { - p.Set(key, value) - } -} - -func (p *keyValueStore) Get(key Key) string { - p.lock.RLock() - defer p.lock.RUnlock() - - return p.vals[key] -} - -func (p *keyValueStore) GetBool(key Key) bool { - return p.Get(key) == "true" -} - -func (p *keyValueStore) GetInt(key Key) int { - if p.Get(key) == "" { - return 0 - } - - value, err := strconv.Atoi(p.Get(key)) - if err != nil { - logrus.WithError(err).Error("Cannot parse int") - } - - return value -} - -func (p *keyValueStore) GetFloat64(key Key) float64 { - if p.Get(key) == "" { - return 0 - } - - value, err := strconv.ParseFloat(p.Get(key), 64) - if err != nil { - logrus.WithError(err).Error("Cannot parse float64") - } - - return value -} - -func (p *keyValueStore) Set(key Key, value string) { - p.lock.Lock() - p.vals[key] = value - p.lock.Unlock() - - if err := p.save(); err != nil { - logrus.WithError(err).Warn("Cannot save preferences") - } -} - -func (p *keyValueStore) SetBool(key Key, value bool) { - if value { - p.Set(key, "true") - } else { - p.Set(key, "false") - } -} - -func (p *keyValueStore) SetInt(key Key, value int) { - p.Set(key, strconv.Itoa(value)) -} - -func (p *keyValueStore) SetFloat64(key Key, value float64) { - p.Set(key, fmt.Sprintf("%v", value)) -} diff --git a/internal/config/settings/kvs_test.go b/internal/config/settings/kvs_test.go deleted file mode 100644 index 2fb75067..00000000 --- a/internal/config/settings/kvs_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package settings - -import ( - "os" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestLoadNoKeyValueStore(t *testing.T) { - r := require.New(t) - pref, clean := newTestEmptyKeyValueStore(r) - defer clean() - - r.Equal("", pref.Get("key")) -} - -func TestLoadBadKeyValueStore(t *testing.T) { - r := require.New(t) - path, clean := newTmpFile(r) - defer clean() - - r.NoError(os.WriteFile(path, []byte("{\"key\":\"MISSING_QUOTES"), 0o700)) - pref := newKeyValueStore(path) - r.Equal("", pref.Get("key")) -} - -func TestKeyValueStor(t *testing.T) { - r := require.New(t) - pref, clean := newTestKeyValueStore(r) - defer clean() - - r.Equal("value", pref.Get("str")) - r.Equal("42", pref.Get("int")) - r.Equal("true", pref.Get("bool")) - r.Equal("t", pref.Get("falseBool")) -} - -func TestKeyValueStoreGetInt(t *testing.T) { - r := require.New(t) - pref, clean := newTestKeyValueStore(r) - defer clean() - - r.Equal(0, pref.GetInt("str")) - r.Equal(42, pref.GetInt("int")) - r.Equal(0, pref.GetInt("bool")) - r.Equal(0, pref.GetInt("falseBool")) -} - -func TestKeyValueStoreGetBool(t *testing.T) { - r := require.New(t) - pref, clean := newTestKeyValueStore(r) - defer clean() - - r.Equal(false, pref.GetBool("str")) - r.Equal(false, pref.GetBool("int")) - r.Equal(true, pref.GetBool("bool")) - r.Equal(false, pref.GetBool("falseBool")) -} - -func TestKeyValueStoreSetDefault(t *testing.T) { - r := require.New(t) - pref, clean := newTestEmptyKeyValueStore(r) - defer clean() - - pref.setDefault("key", "value") - pref.setDefault("key", "othervalue") - r.Equal("value", pref.Get("key")) -} - -func TestKeyValueStoreSet(t *testing.T) { - r := require.New(t) - pref, clean := newTestEmptyKeyValueStore(r) - defer clean() - - pref.Set("str", "value") - checkSavedKeyValueStore(r, pref.path, "{\n\t\"str\": \"value\"\n}") -} - -func TestKeyValueStoreSetInt(t *testing.T) { - r := require.New(t) - pref, clean := newTestEmptyKeyValueStore(r) - defer clean() - - pref.SetInt("int", 42) - checkSavedKeyValueStore(r, pref.path, "{\n\t\"int\": \"42\"\n}") -} - -func TestKeyValueStoreSetBool(t *testing.T) { - r := require.New(t) - pref, clean := newTestEmptyKeyValueStore(r) - defer clean() - - pref.SetBool("trueBool", true) - pref.SetBool("falseBool", false) - checkSavedKeyValueStore(r, pref.path, "{\n\t\"falseBool\": \"false\",\n\t\"trueBool\": \"true\"\n}") -} - -func newTmpFile(r *require.Assertions) (path string, clean func()) { - tmpfile, err := os.CreateTemp("", "pref.*.json") - r.NoError(err) - defer r.NoError(tmpfile.Close()) - - return tmpfile.Name(), func() { - r.NoError(os.Remove(tmpfile.Name())) - } -} - -func newTestEmptyKeyValueStore(r *require.Assertions) (*keyValueStore, func()) { - path, clean := newTmpFile(r) - return newKeyValueStore(path), clean -} - -func newTestKeyValueStore(r *require.Assertions) (*keyValueStore, func()) { - path, clean := newTmpFile(r) - r.NoError(os.WriteFile(path, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0o700)) - return newKeyValueStore(path), clean -} - -func checkSavedKeyValueStore(r *require.Assertions, path, expected string) { - data, err := os.ReadFile(path) - r.NoError(err) - r.Equal(expected, string(data)) -} diff --git a/internal/config/settings/settings.go b/internal/config/settings/settings.go deleted file mode 100644 index 1ee5c373..00000000 --- a/internal/config/settings/settings.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package settings provides access to persistent user settings. -package settings - -import ( - "fmt" - "math/rand" - "path/filepath" - "time" -) - -type Key string - -// Keys of preferences in JSON file. -const ( - FirstStartKey Key = "first_time_start" - FirstStartGUIKey Key = "first_time_start_gui" - LastHeartbeatKey Key = "last_heartbeat" - APIPortKey Key = "user_port_api" - IMAPPortKey Key = "user_port_imap" - SMTPPortKey Key = "user_port_smtp" - SMTPSSLKey Key = "user_ssl_smtp" - AllowProxyKey Key = "allow_proxy" - AutostartKey Key = "autostart" - AutoUpdateKey Key = "autoupdate" - CookiesKey Key = "cookies" - LastVersionKey Key = "last_used_version" - UpdateChannelKey Key = "update_channel" - RolloutKey Key = "rollout" - PreferredKeychainKey Key = "preferred_keychain" - CacheEnabledKey Key = "cache_enabled" - CacheCompressionKey Key = "cache_compression" - CacheLocationKey Key = "cache_location" - CacheMinFreeAbsKey Key = "cache_min_free_abs" - CacheMinFreeRatKey Key = "cache_min_free_rat" - CacheConcurrencyRead Key = "cache_concurrent_read" - CacheConcurrencyWrite Key = "cache_concurrent_write" - IMAPWorkers Key = "imap_workers" - FetchWorkers Key = "fetch_workers" - AttachmentWorkers Key = "attachment_workers" - ColorScheme Key = "color_scheme" - RebrandingMigrationKey Key = "rebranding_migrated" - IsAllMailVisible Key = "is_all_mail_visible" -) - -type Settings struct { - *keyValueStore - - settingsPath string -} - -func New(settingsPath string) *Settings { - s := &Settings{ - keyValueStore: newKeyValueStore(filepath.Join(settingsPath, "prefs.json")), - settingsPath: settingsPath, - } - - s.setDefaultValues() - - return s -} - -const ( - DefaultIMAPPort = "1143" - DefaultSMTPPort = "1025" - DefaultAPIPort = "1042" -) - -func (s *Settings) setDefaultValues() { - s.setDefault(FirstStartKey, "true") - s.setDefault(FirstStartGUIKey, "true") - s.setDefault(LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay())) - s.setDefault(AllowProxyKey, "true") - s.setDefault(AutostartKey, "true") - s.setDefault(AutoUpdateKey, "true") - s.setDefault(LastVersionKey, "") - s.setDefault(UpdateChannelKey, "") - s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64())) //nolint:gosec // G404 It is OK to use weak random number generator here - s.setDefault(PreferredKeychainKey, "") - s.setDefault(CacheEnabledKey, "true") - s.setDefault(CacheCompressionKey, "true") - s.setDefault(CacheLocationKey, "") - s.setDefault(CacheMinFreeAbsKey, "250000000") - s.setDefault(CacheMinFreeRatKey, "") - s.setDefault(CacheConcurrencyRead, "16") - s.setDefault(CacheConcurrencyWrite, "16") - s.setDefault(IMAPWorkers, "16") - s.setDefault(FetchWorkers, "16") - s.setDefault(AttachmentWorkers, "16") - s.setDefault(ColorScheme, "") - - s.setDefault(APIPortKey, DefaultAPIPort) - s.setDefault(IMAPPortKey, DefaultIMAPPort) - s.setDefault(SMTPPortKey, DefaultSMTPPort) - - // By default, stick to STARTTLS. If the user uses catalina+applemail they'll have to change to SSL. - s.setDefault(SMTPSSLKey, "false") - - s.setDefault(IsAllMailVisible, "true") -} diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 7d5710ae..ad44045b 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -18,17 +18,35 @@ // Package constants contains variables that are set via ldflags during build. package constants -import "fmt" +import ( + "fmt" + "runtime" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) const VendorName = "protonmail" //nolint:gochecknoglobals var ( - // Version of the build. + // Full app name (to show to the user). FullAppName = "" + // ConfigName determines the name of the location where bridge stores config files. + ConfigName = "bridge" + + // UpdateName is the name of the product appearing in the update URL. + UpdateName = "bridge" + + // KeyChainName is the name of the entry in the OS keychain. + KeyChainName = "bridge" + // Version of the build. - Version = "" + Version = "2.3.0+git" + + // AppVersion is the full rendered version of the app (to be used in request headers). + AppVersion = getAPIOS() + cases.Title(language.Und).String(ConfigName) + "_" + Version // Revision is current hash of the build. Revision = "" @@ -36,9 +54,31 @@ var ( // BuildTime stamp of the build. BuildTime = "" + // BuildVersion is derived from LongVersion and BuildTime. + BuildVersion = fmt.Sprintf("%v (%v) %v", Version, Revision, BuildTime) + // DSNSentry client keys to be able to report crashes to Sentry. DSNSentry = "" - // BuildVersion is derived from LongVersion and BuildTime. - BuildVersion = fmt.Sprintf("%v (%v) %v", Version, Revision, BuildTime) + // APIHost is our API address. + APIHost = "https://api.protonmail.ch" + + // The host name of the bridge server. + Host = "127.0.0.1" ) + +func getAPIOS() string { + switch runtime.GOOS { + case "darwin": + return "macOS" + + case "linux": + return "Linux" + + case "windows": + return "Windows" + + default: + return "Linux" + } +} diff --git a/internal/constants/update_default.go b/internal/constants/update_default.go index 57dadc36..130ae31a 100644 --- a/internal/constants/update_default.go +++ b/internal/constants/update_default.go @@ -22,8 +22,5 @@ package constants import "time" -//nolint:gochecknoglobals -var ( - // UpdateCheckInterval defines how often we check for new version. - UpdateCheckInterval = time.Hour //nolint:gochecknoglobals -) +// UpdateCheckInterval defines how often we check for new version. +const UpdateCheckInterval = time.Hour diff --git a/internal/constants/update_qa.go b/internal/constants/update_qa.go index 560e798e..d3df68a7 100644 --- a/internal/constants/update_qa.go +++ b/internal/constants/update_qa.go @@ -22,8 +22,5 @@ package constants import "time" -//nolint:gochecknoglobals -var ( - // UpdateCheckInterval defines how often we check for new version - UpdateCheckInterval = time.Duration(5 * time.Minute) -) +// UpdateCheckInterval defines how often we check for new version +const UpdateCheckInterval = time.Duration(5 * time.Minute) diff --git a/internal/cookies/jar.go b/internal/cookies/jar.go index 9d1d2493..acff7091 100644 --- a/internal/cookies/jar.go +++ b/internal/cookies/jar.go @@ -26,28 +26,31 @@ import ( "net/url" "sync" "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" ) type cookiesByHost map[string][]*http.Cookie +type Persister interface { + GetCookies() ([]byte, error) + SetCookies([]byte) error +} + // Jar implements http.CookieJar by wrapping the standard library's cookiejar.Jar. // The jar uses a pantry to load cookies at startup and save cookies when set. type Jar struct { - jar *cookiejar.Jar - settings *settings.Settings - cookies cookiesByHost - locker sync.Locker + jar *cookiejar.Jar + persister Persister + cookies cookiesByHost + locker sync.Locker } -func NewCookieJar(s *settings.Settings) (*Jar, error) { +func NewCookieJar(persister Persister) (*Jar, error) { jar, err := cookiejar.New(nil) if err != nil { return nil, err } - cookiesByHost, err := loadCookies(s) + cookiesByHost, err := loadCookies(persister) if err != nil { return nil, err } @@ -62,10 +65,10 @@ func NewCookieJar(s *settings.Settings) (*Jar, error) { } return &Jar{ - jar: jar, - settings: s, - cookies: cookiesByHost, - locker: &sync.Mutex{}, + jar: jar, + persister: persister, + cookies: cookiesByHost, + locker: &sync.Mutex{}, }, nil } @@ -101,16 +104,17 @@ func (j *Jar) PersistCookies() error { return err } - j.settings.Set(settings.CookiesKey, string(rawCookies)) - - return nil + return j.persister.SetCookies(rawCookies) } // loadCookies loads all non-expired cookies from disk. -func loadCookies(s *settings.Settings) (cookiesByHost, error) { - rawCookies := s.Get(settings.CookiesKey) +func loadCookies(persister Persister) (cookiesByHost, error) { + rawCookies, err := persister.GetCookies() + if err != nil { + return nil, err + } - if rawCookies == "" { + if len(rawCookies) == 0 { return make(cookiesByHost), nil } diff --git a/internal/cookies/jar_test.go b/internal/cookies/jar_test.go index 35d1609e..e41488b3 100644 --- a/internal/cookies/jar_test.go +++ b/internal/cookies/jar_test.go @@ -18,13 +18,15 @@ package cookies import ( + "errors" + "io/fs" "net/http" "net/http/httptest" "os" + "path/filepath" "testing" "time" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,7 +39,7 @@ func TestJarGetSet(t *testing.T) { }) defer ts.Close() - client, _ := getClientWithJar(t, newFakeSettings()) + client, _ := getClientWithJar(t, newTestPersister(t)) // Hit a server that sets some cookies. setRes, err := client.Get(ts.URL + "/set") @@ -63,7 +65,7 @@ func TestJarLoad(t *testing.T) { defer ts.Close() // This will be our "persistent storage" from which the cookie jar should load cookies. - s := newFakeSettings() + s := newTestPersister(t) // This client saves cookies to persistent storage. oldClient, jar := getClientWithJar(t, s) @@ -98,7 +100,7 @@ func TestJarExpiry(t *testing.T) { defer ts.Close() // This will be our "persistent storage" from which the cookie jar should load cookies. - s := newFakeSettings() + s := newTestPersister(t) // This client saves cookies to persistent storage. oldClient, jar1 := getClientWithJar(t, s) @@ -122,9 +124,12 @@ func TestJarExpiry(t *testing.T) { // Save the cookies (expired ones were cleared out). require.NoError(t, jar2.PersistCookies()) - assert.Contains(t, s.Get(settings.CookiesKey), "TestName1") - assert.NotContains(t, s.Get(settings.CookiesKey), "TestName2") - assert.Contains(t, s.Get(settings.CookiesKey), "TestName3") + cookies, err := s.GetCookies() + require.NoError(t, err) + + assert.Contains(t, string(cookies), "TestName1") + assert.NotContains(t, string(cookies), "TestName2") + assert.Contains(t, string(cookies), "TestName3") } type testCookie struct { @@ -132,8 +137,8 @@ type testCookie struct { maxAge int } -func getClientWithJar(t *testing.T, s *settings.Settings) (*http.Client, *Jar) { - jar, err := NewCookieJar(s) +func getClientWithJar(t *testing.T, persister Persister) (*http.Client, *Jar) { + jar, err := NewCookieJar(persister) require.NoError(t, err) return &http.Client{Jar: jar}, jar @@ -168,12 +173,26 @@ func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server { return httptest.NewServer(mux) } -// newFakeSettings creates a temporary folder for files. -func newFakeSettings() *settings.Settings { - dir, err := os.MkdirTemp("", "test-settings") - if err != nil { - panic(err) +type testPersister struct { + path string +} + +func newTestPersister(tb testing.TB) *testPersister { + path := filepath.Join(tb.TempDir(), "cookies.json") + + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + if err := os.WriteFile(path, []byte{}, 0600); err != nil { + panic(err) + } } - return settings.New(dir) + return &testPersister{path: path} +} + +func (p *testPersister) GetCookies() ([]byte, error) { + return os.ReadFile(p.path) +} + +func (p *testPersister) SetCookies(rawCookies []byte) error { + return os.WriteFile(p.path, rawCookies, 0600) } diff --git a/internal/crash/handler.go b/internal/crash/handler.go index ae27821b..df4a4478 100644 --- a/internal/crash/handler.go +++ b/internal/crash/handler.go @@ -41,14 +41,11 @@ func (h *Handler) AddRecoveryAction(action RecoveryAction) *Handler { func (h *Handler) HandlePanic() { sentry.SkipDuringUnwind() - r := recover() - if r == nil { - return - } - - for _, action := range h.actions { - if err := action(r); err != nil { - logrus.WithError(err).Error("Failed to execute recovery action") + if r := recover(); r != nil { + for _, action := range h.actions { + if err := action(r); err != nil { + logrus.WithError(err).Error("Failed to execute recovery action") + } } } } diff --git a/pkg/pmapi/dialer_basic.go b/internal/dialer/dialer_basic.go similarity index 74% rename from pkg/pmapi/dialer_basic.go rename to internal/dialer/dialer_basic.go index 552393e4..65b64ceb 100644 --- a/pkg/pmapi/dialer_basic.go +++ b/internal/dialer/dialer_basic.go @@ -15,9 +15,10 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( + "context" "crypto/tls" "net" "net/http" @@ -25,13 +26,13 @@ import ( ) type TLSDialer interface { - DialTLS(network, address string) (conn net.Conn, err error) + DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) } // CreateTransportWithDialer creates an http.Transport that uses the given dialer to make TLS connections. func CreateTransportWithDialer(dialer TLSDialer) *http.Transport { return &http.Transport{ - DialTLS: dialer.DialTLS, + DialTLSContext: dialer.DialTLSContext, Proxy: http.ProxyFromEnvironment, MaxIdleConns: 100, @@ -53,26 +54,24 @@ func CreateTransportWithDialer(dialer TLSDialer) *http.Transport { // BasicTLSDialer implements TLSDialer. type BasicTLSDialer struct { - cfg Config + hostURL string } // NewBasicTLSDialer returns a new BasicTLSDialer. -func NewBasicTLSDialer(cfg Config) *BasicTLSDialer { +func NewBasicTLSDialer(hostURL string) *BasicTLSDialer { return &BasicTLSDialer{ - cfg: cfg, + hostURL: hostURL, } } // DialTLS returns a connection to the given address using the given network. -func (d *BasicTLSDialer) DialTLS(network, address string) (conn net.Conn, err error) { - dialer := &net.Dialer{Timeout: 30 * time.Second} // Alternative Routes spec says this should be a 30s timeout. - - var tlsConfig *tls.Config - - // If we are not dialing the standard API then we should skip cert verification checks. - if address != d.cfg.HostURL { - tlsConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec - } - - return tls.DialWithDialer(dialer, network, address, tlsConfig) +func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) { + return (&tls.Dialer{ + NetDialer: &net.Dialer{ + Timeout: 30 * time.Second, + }, + Config: &tls.Config{ + InsecureSkipVerify: address != d.hostURL, + }, + }).DialContext(ctx, network, address) } diff --git a/pkg/pmapi/dialer_pinning.go b/internal/dialer/dialer_pinning.go similarity index 70% rename from pkg/pmapi/dialer_pinning.go rename to internal/dialer/dialer_pinning.go index 9daa5306..a137848f 100644 --- a/pkg/pmapi/dialer_pinning.go +++ b/internal/dialer/dialer_pinning.go @@ -15,13 +15,12 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( + "context" "crypto/tls" "net" - - "github.com/sirupsen/logrus" ) // TrustedAPIPins contains trusted public keys of the protonmail API and proxies. @@ -56,36 +55,37 @@ const TLSReportURI = "https://reports.protonmail.ch/reports/tls" // PinningTLSDialer wraps a TLSDialer to check fingerprints after connecting and // to report errors if the fingerprint check fails. type PinningTLSDialer struct { - dialer TLSDialer - - // pinChecker is used to check TLS keys of connections. - pinChecker *pinChecker - - reporter *tlsReporter - - // tlsIssueNotifier is used to notify something when there is a TLS issue. - tlsIssueNotifier func() - - // A logger for logging messages. - log logrus.FieldLogger + dialer TLSDialer + pinChecker PinChecker + reporter Reporter + tlsIssueCh chan struct{} } -// NewPinningTLSDialer constructs a new dialer which only returns tcp connections to servers +// Reporter is used to report TLS issues. +type Reporter interface { + ReportCertIssue(reportURI, host, port string, state tls.ConnectionState) +} + +// PinChecker is used to check TLS keys of connections. +type PinChecker interface { + CheckCertificate(conn net.Conn) error +} + +// NewPinningTLSDialer constructs a new dialer which only returns TCP connections to servers // which present known certificates. -// If enabled, it reports any invalid certificates it finds. -func NewPinningTLSDialer(cfg Config, dialer TLSDialer) *PinningTLSDialer { +// It checks pins using the given pinChecker and reports issues using the given reporter. +func NewPinningTLSDialer(dialer TLSDialer, reporter Reporter, pinChecker PinChecker) *PinningTLSDialer { return &PinningTLSDialer{ - dialer: dialer, - pinChecker: newPinChecker(TrustedAPIPins), - reporter: newTLSReporter(cfg, TrustedAPIPins), - tlsIssueNotifier: cfg.TLSIssueHandler, - log: logrus.WithField("pkg", "pmapi/tls-pinning"), + dialer: dialer, + pinChecker: pinChecker, + reporter: reporter, + tlsIssueCh: make(chan struct{}, 1), } } // DialTLS dials the given network/address, returning an error if the certificates don't match the trusted pins. -func (p *PinningTLSDialer) DialTLS(network, address string) (net.Conn, error) { - conn, err := p.dialer.DialTLS(network, address) +func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) { + conn, err := p.dialer.DialTLSContext(ctx, network, address) if err != nil { return nil, err } @@ -95,22 +95,20 @@ func (p *PinningTLSDialer) DialTLS(network, address string) (net.Conn, error) { return nil, err } - if err := p.pinChecker.checkCertificate(conn); err != nil { - if p.tlsIssueNotifier != nil { - go p.tlsIssueNotifier() + if err := p.pinChecker.CheckCertificate(conn); err != nil { + if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil { + p.reporter.ReportCertIssue(TLSReportURI, host, port, tlsConn.ConnectionState()) } - if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil { - p.reporter.reportCertIssue( - TLSReportURI, - host, - port, - tlsConn.ConnectionState(), - ) - } + p.tlsIssueCh <- struct{}{} return nil, err } return conn, nil } + +// GetTLSIssueCh returns a channel which notifies when a TLS issue is reported. +func (p *PinningTLSDialer) GetTLSIssueCh() <-chan struct{} { + return p.tlsIssueCh +} diff --git a/pkg/pmapi/dialer_pinning_checker.go b/internal/dialer/dialer_pinning_checker.go similarity index 89% rename from pkg/pmapi/dialer_pinning_checker.go rename to internal/dialer/dialer_pinning_checker.go index 79027e8f..571e58a3 100644 --- a/pkg/pmapi/dialer_pinning_checker.go +++ b/internal/dialer/dialer_pinning_checker.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( "crypto/tls" @@ -30,18 +30,18 @@ import ( // ErrTLSMismatch indicates that no TLS fingerprint match could be found. var ErrTLSMismatch = errors.New("no TLS fingerprint match found") -type pinChecker struct { +type TLSPinChecker struct { trustedPins []string } -func newPinChecker(trustedPins []string) *pinChecker { - return &pinChecker{ +func NewTLSPinChecker(trustedPins []string) *TLSPinChecker { + return &TLSPinChecker{ trustedPins: trustedPins, } } // checkCertificate returns whether the connection presents a known TLS certificate. -func (p *pinChecker) checkCertificate(conn net.Conn) error { +func (p *TLSPinChecker) CheckCertificate(conn net.Conn) error { tlsConn, ok := conn.(*tls.Conn) if !ok { return errors.New("connection is not a TLS connection") diff --git a/pkg/pmapi/dialer_pinning_report.go b/internal/dialer/dialer_pinning_report.go similarity index 74% rename from pkg/pmapi/dialer_pinning_report.go rename to internal/dialer/dialer_pinning_report.go index 8d9c5143..d3eee656 100644 --- a/pkg/pmapi/dialer_pinning_report.go +++ b/internal/dialer/dialer_pinning_report.go @@ -15,17 +15,13 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( - "bytes" - "encoding/json" - "io" - "net/http" - "strconv" + "fmt" "time" - "github.com/sirupsen/logrus" + "github.com/go-resty/resty/v2" ) // tlsReport is inspired by https://tools.ietf.org/html/rfc7469#section-3. @@ -38,7 +34,7 @@ type tlsReport struct { Hostname string `json:"hostname"` // Port to which the UA made original request that failed pin validation. - Port int `json:"port"` + Port string `json:"port"` // EffectiveExpirationDate for noted pins in time.RFC3339 format. EffectiveExpirationDate string `json:"effective-expiration-date"` @@ -89,12 +85,9 @@ type tlsReport struct { // newTLSReport constructs a new tlsReport configured with the given app version and known pinned public keys. // Temporal things (current date/time) are not set yet -- they are set when sendReport is called. func newTLSReport(host, port, server string, certChain, knownPins []string, appVersion string) (report tlsReport) { - // If we can't parse the port for whatever reason, it doesn't really matter; we should report anyway. - intPort, _ := strconv.Atoi(port) - report = tlsReport{ Hostname: host, - Port: intPort, + Port: port, NotedHostname: server, ServedCertificateChain: certChain, KnownPins: knownPins, @@ -105,40 +98,21 @@ func newTLSReport(host, port, server string, certChain, knownPins []string, appV } // sendReport posts the given TLS report to the standard TLS Report URI. -func (r tlsReport) sendReport(cfg Config, uri string) { +func sendReport(report tlsReport, userAgent, appVersion, hostURL, remoteURI string) error { now := time.Now() - r.DateTime = now.Format(time.RFC3339) - r.EffectiveExpirationDate = now.Add(365 * 24 * 60 * 60 * time.Second).Format(time.RFC3339) - b, err := json.Marshal(r) - if err != nil { - logrus.WithError(err).Error("Failed to marshal TLS report") - return + report.DateTime = now.Format(time.RFC3339) + report.EffectiveExpirationDate = now.Add(365 * 24 * time.Hour).Format(time.RFC3339) + + if _, err := resty.New(). + SetTransport(CreateTransportWithDialer(NewBasicTLSDialer(hostURL))). + SetHeader("User-Agent", userAgent). + SetHeader("x-pm-appversion", appVersion). + NewRequest(). + SetBody(report). + Post(remoteURI); err != nil { + return fmt.Errorf("failed to send TLS report: %w", err) } - req, err := http.NewRequest("POST", uri, bytes.NewReader(b)) - if err != nil { - logrus.WithError(err).Error("Failed to create http request") - return - } - - req.Header.Add("Content-Type", "application/json") - req.Header.Set("User-Agent", cfg.getUserAgent()) - req.Header.Set("x-pm-appversion", r.AppVersion) - - logrus.WithField("request", req).Warn("Reporting TLS mismatch") - res, err := (&http.Client{Transport: CreateTransportWithDialer(NewBasicTLSDialer(cfg))}).Do(req) - if err != nil { - logrus.WithError(err).Error("Failed to report TLS mismatch") - return - } - - logrus.WithField("response", res).Error("Reported TLS mismatch") - - if res.StatusCode != http.StatusOK { - logrus.WithField("status", http.StatusOK).Error("StatusCode was not OK") - } - - _, _ = io.ReadAll(res.Body) - _ = res.Body.Close() + return nil } diff --git a/pkg/pmapi/dialer_pinning_reporter.go b/internal/dialer/dialer_pinning_reporter.go similarity index 73% rename from pkg/pmapi/dialer_pinning_reporter.go rename to internal/dialer/dialer_pinning_reporter.go index 4daa3395..3333573c 100644 --- a/pkg/pmapi/dialer_pinning_reporter.go +++ b/internal/dialer/dialer_pinning_reporter.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( "bytes" @@ -24,6 +24,7 @@ import ( "encoding/pem" "time" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" "github.com/google/go-cmp/cmp" "github.com/sirupsen/logrus" ) @@ -33,21 +34,25 @@ type sentReport struct { t time.Time } -type tlsReporter struct { - cfg Config +type TLSReporter struct { + hostURL string + appVersion string + userAgent *useragent.UserAgent trustedPins []string sentReports []sentReport } -func newTLSReporter(cfg Config, trustedPins []string) *tlsReporter { - return &tlsReporter{ - cfg: cfg, +func NewTLSReporter(hostURL, appVersion string, userAgent *useragent.UserAgent, trustedPins []string) *TLSReporter { + return &TLSReporter{ + hostURL: hostURL, + appVersion: appVersion, + userAgent: userAgent, trustedPins: trustedPins, } } // reportCertIssue reports a TLS key mismatch. -func (r *tlsReporter) reportCertIssue(remoteURI, host, port string, connState tls.ConnectionState) { +func (r *TLSReporter) ReportCertIssue(remoteURI, host, port string, connState tls.ConnectionState) { var certChain []string if len(connState.VerifiedChains) > 0 { @@ -56,16 +61,19 @@ func (r *tlsReporter) reportCertIssue(remoteURI, host, port string, connState tl certChain = marshalCert7468(connState.PeerCertificates) } - report := newTLSReport(host, port, connState.ServerName, certChain, r.trustedPins, r.cfg.AppVersion) + report := newTLSReport(host, port, connState.ServerName, certChain, r.trustedPins, r.appVersion) if !r.hasRecentlySentReport(report) { r.recordReport(report) - go report.sendReport(r.cfg, remoteURI) + + if err := sendReport(report, r.userAgent.GetUserAgent(), r.appVersion, r.hostURL, remoteURI); err != nil { + logrus.WithError(err).Error("Failed to send TLS pinning report") + } } } // hasRecentlySentReport returns whether the report was already sent within the last 24 hours. -func (r *tlsReporter) hasRecentlySentReport(report tlsReport) bool { +func (r *TLSReporter) hasRecentlySentReport(report tlsReport) bool { var validReports []sentReport for _, r := range r.sentReports { @@ -86,7 +94,7 @@ func (r *tlsReporter) hasRecentlySentReport(report tlsReport) bool { } // recordReport records the given report and the current time so we can check whether we recently sent this report. -func (r *tlsReporter) recordReport(report tlsReport) { +func (r *TLSReporter) recordReport(report tlsReport) { r.sentReports = append(r.sentReports, sentReport{r: report, t: time.Now()}) } @@ -97,7 +105,7 @@ func marshalCert7468(certs []*x509.Certificate) (pemCerts []string) { Type: "CERTIFICATE", Bytes: cert.Raw, }); err != nil { - logrus.WithField("pkg", "pmapi/tls-pinning").WithError(err).Error("Failed to encode TLS certificate") + logrus.WithError(err).Error("Failed to encode TLS certificate") } pemCerts = append(pemCerts, buffer.String()) buffer.Reset() diff --git a/pkg/pmapi/dialer_pinning_reporter_test.go b/internal/dialer/dialer_pinning_reporter_test.go similarity index 84% rename from pkg/pmapi/dialer_pinning_reporter_test.go rename to internal/dialer/dialer_pinning_reporter_test.go index 196a7f3a..75d5c136 100644 --- a/pkg/pmapi/dialer_pinning_reporter_test.go +++ b/internal/dialer/dialer_pinning_reporter_test.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( "crypto/tls" @@ -24,6 +24,7 @@ import ( "testing" "time" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" "github.com/stretchr/testify/assert" ) @@ -34,15 +35,11 @@ func TestTLSReporter_DoubleReport(t *testing.T) { reportCounter++ })) - cfg := Config{ - AppVersion: "3", - UserAgent: "useragent", - } - r := newTLSReporter(cfg, TrustedAPIPins) + r := NewTLSReporter("hostURL", "appVersion", useragent.New(), TrustedAPIPins) // Report the same issue many times. for i := 0; i < 10; i++ { - r.reportCertIssue(reportServer.URL, "myhost", "443", tls.ConnectionState{}) + r.ReportCertIssue(reportServer.URL, "myhost", "443", tls.ConnectionState{}) } // We should only report once. @@ -52,7 +49,7 @@ func TestTLSReporter_DoubleReport(t *testing.T) { // If we then report something else many times. for i := 0; i < 10; i++ { - r.reportCertIssue(reportServer.URL, "anotherhost", "443", tls.ConnectionState{}) + r.ReportCertIssue(reportServer.URL, "anotherhost", "443", tls.ConnectionState{}) } // We should get a second report. diff --git a/pkg/pmapi/dialer_pinning_test.go b/internal/dialer/dialer_pinning_test.go similarity index 51% rename from pkg/pmapi/dialer_pinning_test.go rename to internal/dialer/dialer_pinning_test.go index c61ef9eb..62b3537e 100644 --- a/pkg/pmapi/dialer_pinning_test.go +++ b/internal/dialer/dialer_pinning_test.go @@ -15,112 +15,120 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( "context" - "net/http" - "net/http/httptest" "testing" "time" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" a "github.com/stretchr/testify/assert" r "github.com/stretchr/testify/require" + "gitlab.protontech.ch/go/liteapi" + "gitlab.protontech.ch/go/liteapi/server" ) -func TestTLSPinValid(t *testing.T) { - called, _, cm := createClientWithPinningDialer(getRootURL()) +func getRootURL() string { + return "https://api.protonmail.ch" +} + +func TestTLSPinValid(t *testing.T) { + called, _, _, _, cm := createClientWithPinningDialer(getRootURL()) + + _, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password") - _, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"}) checkTLSIssueHandler(t, 0, called) } func TestTLSPinBackup(t *testing.T) { - called, dialer, cm := createClientWithPinningDialer(getRootURL()) - copyTrustedPins(dialer.pinChecker) - dialer.pinChecker.trustedPins[1] = dialer.pinChecker.trustedPins[0] - dialer.pinChecker.trustedPins[0] = "" + called, _, _, checker, cm := createClientWithPinningDialer(getRootURL()) + copyTrustedPins(checker) + checker.trustedPins[1] = checker.trustedPins[0] + checker.trustedPins[0] = "" + + _, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password") - _, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"}) checkTLSIssueHandler(t, 0, called) } func TestTLSPinInvalid(t *testing.T) { - ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeJSONResponsefromFile(t, w, "/auth/info/post_response.json", 0) - })) - defer ts.Close() + s := server.NewTLS() + defer s.Close() - called, _, cm := createClientWithPinningDialer(ts.URL) + called, _, _, _, cm := createClientWithPinningDialer(s.GetHostURL()) + + _, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password") - _, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"}) checkTLSIssueHandler(t, 1, called) } func TestTLSPinNoMatch(t *testing.T) { skipIfProxyIsSet(t) - called, dialer, cm := createClientWithPinningDialer(getRootURL()) + called, _, reporter, checker, cm := createClientWithPinningDialer(getRootURL()) - copyTrustedPins(dialer.pinChecker) - for i := 0; i < len(dialer.pinChecker.trustedPins); i++ { - dialer.pinChecker.trustedPins[i] = "testing" + copyTrustedPins(checker) + for i := 0; i < len(checker.trustedPins); i++ { + checker.trustedPins[i] = "testing" } - _, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"}) - _, _ = cm.getAuthInfo(context.Background(), GetAuthInfoReq{Username: "username"}) + _, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password") + _, _, _ = cm.NewClientWithLogin(context.Background(), "username", "password") // Check that it will be reported only once per session, but notified every time. - r.Equal(t, 1, len(dialer.reporter.sentReports)) + r.Equal(t, 1, len(reporter.sentReports)) checkTLSIssueHandler(t, 2, called) } func TestTLSSignedCertWrongPublicKey(t *testing.T) { skipIfProxyIsSet(t) - _, dialer, _ := createClientWithPinningDialer("") - _, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443") + _, dialer, _, _, _ := createClientWithPinningDialer("") + _, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443") r.Error(t, err, "expected dial to fail because of wrong public key") } func TestTLSSignedCertTrustedPublicKey(t *testing.T) { skipIfProxyIsSet(t) - _, dialer, _ := createClientWithPinningDialer("") - copyTrustedPins(dialer.pinChecker) - dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="LwnIKjNLV3z243ap8y0yXNPghsqE76J08Eq3COvUt2E="`) - _, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443") + _, dialer, _, checker, _ := createClientWithPinningDialer("") + copyTrustedPins(checker) + checker.trustedPins = append(checker.trustedPins, `pin-sha256="LwnIKjNLV3z243ap8y0yXNPghsqE76J08Eq3COvUt2E="`) + _, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443") r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA") } func TestTLSSelfSignedCertTrustedPublicKey(t *testing.T) { skipIfProxyIsSet(t) - _, dialer, _ := createClientWithPinningDialer("") - copyTrustedPins(dialer.pinChecker) - dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="9SLklscvzMYj8f+52lp5ze/hY0CFHyLSPQzSpYYIBm8="`) - _, err := dialer.DialTLS("tcp", "self-signed.badssl.com:443") + _, dialer, _, checker, _ := createClientWithPinningDialer("") + copyTrustedPins(checker) + checker.trustedPins = append(checker.trustedPins, `pin-sha256="9SLklscvzMYj8f+52lp5ze/hY0CFHyLSPQzSpYYIBm8="`) + _, err := dialer.DialTLSContext(context.Background(), "tcp", "self-signed.badssl.com:443") r.NoError(t, err, "expected dial to succeed because public key is known despite cert being self-signed") } -func createClientWithPinningDialer(hostURL string) (*int, *PinningTLSDialer, *manager) { +func createClientWithPinningDialer(hostURL string) (*int, *PinningTLSDialer, *TLSReporter, *TLSPinChecker, *liteapi.Manager) { called := 0 - cfg := Config{ - AppVersion: "Bridge_1.2.4-test", - HostURL: hostURL, - TLSIssueHandler: func() { called++ }, - } + reporter := NewTLSReporter(hostURL, "appVersion", useragent.New(), TrustedAPIPins) + checker := NewTLSPinChecker(TrustedAPIPins) + dialer := NewPinningTLSDialer(NewBasicTLSDialer(hostURL), reporter, checker) - dialer := NewPinningTLSDialer(cfg, NewBasicTLSDialer(cfg)) + go func() { + for range dialer.GetTLSIssueCh() { + called++ + } + }() - cm := newManager(cfg) - cm.SetTransport(CreateTransportWithDialer(dialer)) - - return &called, dialer, cm + return &called, dialer, reporter, checker, liteapi.New( + liteapi.WithHostURL(hostURL), + liteapi.WithTransport(CreateTransportWithDialer(dialer)), + ) } -func copyTrustedPins(pinChecker *pinChecker) { +func copyTrustedPins(pinChecker *TLSPinChecker) { copiedPins := make([]string, len(pinChecker.trustedPins)) copy(copiedPins, pinChecker.trustedPins) pinChecker.trustedPins = copiedPins diff --git a/pkg/pmapi/dialer_proxy.go b/internal/dialer/dialer_proxy.go similarity index 85% rename from pkg/pmapi/dialer_proxy.go rename to internal/dialer/dialer_proxy.go index 30cb9867..41a16111 100644 --- a/pkg/pmapi/dialer_proxy.go +++ b/internal/dialer/dialer_proxy.go @@ -15,9 +15,10 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( + "context" "net" "net/url" "sync" @@ -27,6 +28,8 @@ import ( "github.com/sirupsen/logrus" ) +var ErrNoConnection = errors.New("no connection") + // ProxyTLSDialer wraps a TLSDialer to switch to a proxy if the initial dial fails. type ProxyTLSDialer struct { dialer TLSDialer @@ -40,13 +43,13 @@ type ProxyTLSDialer struct { } // NewProxyTLSDialer constructs a dialer which provides a proxy-managing layer on top of an underlying dialer. -func NewProxyTLSDialer(cfg Config, dialer TLSDialer) *ProxyTLSDialer { +func NewProxyTLSDialer(dialer TLSDialer, hostURL string) *ProxyTLSDialer { return &ProxyTLSDialer{ dialer: dialer, locker: sync.RWMutex{}, - directAddress: formatAsAddress(cfg.HostURL), - proxyAddress: formatAsAddress(cfg.HostURL), - proxyProvider: newProxyProvider(cfg, dohProviders, proxyQuery), + directAddress: formatAsAddress(hostURL), + proxyAddress: formatAsAddress(hostURL), + proxyProvider: newProxyProvider(dialer, hostURL, DoHProviders), proxyUseDuration: proxyUseDuration, } } @@ -73,22 +76,21 @@ func formatAsAddress(rawURL string) string { } // DialTLS dials the given network/address. If it fails, it retries using a proxy. -func (d *ProxyTLSDialer) DialTLS(network, address string) (net.Conn, error) { +func (d *ProxyTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) { if address == d.directAddress { address = d.proxyAddress } - conn, err := d.dialer.DialTLS(network, address) + conn, err := d.dialer.DialTLSContext(ctx, network, address) if err == nil || !d.allowProxy { return conn, err } - err = d.switchToReachableServer() - if err != nil { + if err := d.switchToReachableServer(); err != nil { return nil, err } - return d.dialer.DialTLS(network, d.proxyAddress) + return d.dialer.DialTLSContext(ctx, network, d.proxyAddress) } // switchToReachableServer switches to using a reachable server (either proxy or standard API). @@ -128,6 +130,7 @@ func (d *ProxyTLSDialer) switchToReachableServer() error { } d.proxyAddress = proxyAddress + return nil } diff --git a/pkg/pmapi/dialer_proxy_provider.go b/internal/dialer/dialer_proxy_provider.go similarity index 86% rename from pkg/pmapi/dialer_proxy_provider.go rename to internal/dialer/dialer_proxy_provider.go index f79020ed..b37d5361 100644 --- a/pkg/pmapi/dialer_proxy_provider.go +++ b/internal/dialer/dialer_proxy_provider.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( "context" @@ -27,6 +27,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/miekg/dns" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) const ( @@ -35,14 +36,14 @@ const ( proxyCacheRefreshTimeout = 20 * time.Second proxyDoHTimeout = 20 * time.Second proxyCanReachTimeout = 20 * time.Second - proxyQuery = "dMFYGSLTQOJXXI33ONVQWS3BOMNUA.protonpro.xyz" + proxyQuery = "dMFYGSLTQOJXXI33ONVQWS3BOMNUA.protonpro.xyz" Quad9Provider = "https://dns11.quad9.net/dns-query" Quad9PortProvider = "https://dns11.quad9.net:5053/dns-query" GoogleProvider = "https://dns.google/dns-query" ) -var dohProviders = []string{ //nolint:gochecknoglobals +var DoHProviders = []string{ //nolint:gochecknoglobals Quad9Provider, Quad9PortProvider, GoogleProvider, @@ -50,7 +51,9 @@ var dohProviders = []string{ //nolint:gochecknoglobals // proxyProvider manages known proxies. type proxyProvider struct { - cfg Config + dialer TLSDialer + + hostURL string // dohLookup is used to look up the given query at the given DoH provider, returning the TXT records> dohLookup func(ctx context.Context, query, provider string) (urls []string, err error) @@ -68,11 +71,12 @@ type proxyProvider struct { // newProxyProvider creates a new proxyProvider that queries the given DoH providers // to retrieve DNS records for the given query string. -func newProxyProvider(cfg Config, providers []string, query string) (p *proxyProvider) { //nolint:unparam +func newProxyProvider(dialer TLSDialer, hostURL string, providers []string) (p *proxyProvider) { p = &proxyProvider{ - cfg: cfg, + dialer: dialer, + hostURL: hostURL, providers: providers, - query: query, + query: proxyQuery, cacheRefreshTimeout: proxyCacheRefreshTimeout, dohTimeout: proxyDoHTimeout, canReachTimeout: proxyCanReachTimeout, @@ -86,7 +90,7 @@ func newProxyProvider(cfg Config, providers []string, query string) (p *proxyPro // findReachableServer returns a working API server (either proxy or standard API). func (p *proxyProvider) findReachableServer() (proxy string, err error) { - log.Debug("Trying to find a reachable server") + logrus.Debug("Trying to find a reachable server") if time.Now().Before(p.lastLookup.Add(proxyLookupWait)) { return "", errors.New("not looking for a proxy, too soon") @@ -106,7 +110,7 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) { go func() { defer wg.Done() - apiReachable = p.canReach(p.cfg.HostURL) + apiReachable = p.canReach(p.hostURL) }() go func() { @@ -117,7 +121,7 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) { wg.Wait() if apiReachable { - proxy = p.cfg.HostURL + proxy = p.hostURL return } @@ -138,7 +142,7 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) { // refreshProxyCache loads the latest proxies from the known providers. // If the process takes longer than proxyCacheRefreshTimeout, an error is returned. func (p *proxyProvider) refreshProxyCache() error { - log.Info("Refreshing proxy cache") + logrus.Info("Refreshing proxy cache") ctx, cancel := context.WithTimeout(context.Background(), p.cacheRefreshTimeout) defer cancel() @@ -169,21 +173,19 @@ func (p *proxyProvider) refreshProxyCache() error { // canReach returns whether we can reach the given url. func (p *proxyProvider) canReach(url string) bool { - log.WithField("url", url).Debug("Trying to ping proxy") + logrus.WithField("url", url).Debug("Trying to ping proxy") if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { url = "https://" + url } - dialer := NewPinningTLSDialer(p.cfg, NewBasicTLSDialer(p.cfg)) - pinger := resty.New(). SetBaseURL(url). SetTimeout(p.canReachTimeout). - SetTransport(CreateTransportWithDialer(dialer)) + SetTransport(CreateTransportWithDialer(p.dialer)) if _, err := pinger.R().Get("/tests/ping"); err != nil { - log.WithField("proxy", url).WithError(err).Warn("Failed to ping proxy") + logrus.WithField("proxy", url).WithError(err).Warn("Failed to ping proxy") return false } @@ -240,15 +242,15 @@ func (p *proxyProvider) defaultDoHLookup(ctx context.Context, query, dohProvider select { case data = <-dataChan: - log.WithField("data", data).Info("Received TXT records") + logrus.WithField("data", data).Info("Received TXT records") return case err = <-errChan: - log.WithField("provider", dohProvider).WithError(err).Error("Failed to query DNS records") + logrus.WithField("provider", dohProvider).WithError(err).Error("Failed to query DNS records") return case <-ctx.Done(): - log.WithField("provider", dohProvider).Error("Timed out querying DNS records") + logrus.WithField("provider", dohProvider).Error("Timed out querying DNS records") return []string{}, errors.New("timed out querying DNS records") } } diff --git a/pkg/pmapi/dialer_proxy_provider_test.go b/internal/dialer/dialer_proxy_provider_test.go similarity index 78% rename from pkg/pmapi/dialer_proxy_provider_test.go rename to internal/dialer/dialer_proxy_provider_test.go index f60868ca..97df0607 100644 --- a/pkg/pmapi/dialer_proxy_provider_test.go +++ b/internal/dialer/dialer_proxy_provider_test.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( "context" @@ -23,15 +23,15 @@ import ( "testing" "time" + "github.com/ProtonMail/proton-bridge/v2/internal/useragent" r "github.com/stretchr/testify/require" - "golang.org/x/net/http/httpproxy" ) func TestProxyProvider_FindProxy(t *testing.T) { proxy := getTrustedServer() defer closeServer(proxy) - p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used") + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}) p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy.URL}, nil } url, err := p.findReachableServer() @@ -47,7 +47,7 @@ func TestProxyProvider_FindProxy_ChooseReachableProxy(t *testing.T) { unreachableProxy := getTrustedServer() closeServer(unreachableProxy) - p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used") + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}) p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{reachableProxy.URL, unreachableProxy.URL}, nil } @@ -64,7 +64,11 @@ func TestProxyProvider_FindProxy_ChooseTrustedProxy(t *testing.T) { untrustedProxy := getUntrustedServer() defer closeServer(untrustedProxy) - p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used") + reporter := NewTLSReporter("", "appVersion", useragent.New(), TrustedAPIPins) + checker := NewTLSPinChecker(TrustedAPIPins) + dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker) + + p := newProxyProvider(dialer, "", []string{"not used"}) p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{untrustedProxy.URL, trustedProxy.URL}, nil } @@ -81,7 +85,7 @@ func TestProxyProvider_FindProxy_FailIfNoneReachable(t *testing.T) { unreachableProxy2 := getTrustedServer() closeServer(unreachableProxy2) - p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used") + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}) p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{unreachableProxy1.URL, unreachableProxy2.URL}, nil } @@ -97,7 +101,11 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) { untrustedProxy2 := getUntrustedServer() defer closeServer(untrustedProxy2) - p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used") + reporter := NewTLSReporter("", "appVersion", useragent.New(), TrustedAPIPins) + checker := NewTLSPinChecker(TrustedAPIPins) + dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker) + + p := newProxyProvider(dialer, "", []string{"not used"}) p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{untrustedProxy1.URL, untrustedProxy2.URL}, nil } @@ -107,7 +115,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) { } func TestProxyProvider_FindProxy_RefreshCacheTimeout(t *testing.T) { - p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used") + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}) p.cacheRefreshTimeout = 1 * time.Second p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { time.Sleep(2 * time.Second); return nil, nil } @@ -124,7 +132,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) { })) defer closeServer(slowProxy) - p := newProxyProvider(Config{HostURL: ""}, []string{"not used"}, "not used") + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}) p.canReachTimeout = 1 * time.Second p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{slowProxy.URL}, nil } @@ -136,7 +144,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) { } func TestProxyProvider_DoHLookup_Quad9(t *testing.T) { - p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery) + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}) records, err := p.dohLookup(context.Background(), proxyQuery, Quad9Provider) r.NoError(t, err) @@ -147,7 +155,7 @@ func TestProxyProvider_DoHLookup_Quad9(t *testing.T) { // port filter. Basic functionality should be covered by other tests. Keeping // code here to be able to run it locally if needed. func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) { - p := newProxyProvider(Config{}, []string{Quad9PortProvider, GoogleProvider}, proxyQuery) + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}) records, err := p.dohLookup(context.Background(), proxyQuery, Quad9PortProvider) r.NoError(t, err) @@ -155,7 +163,7 @@ func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) { } func TestProxyProvider_DoHLookup_Google(t *testing.T) { - p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery) + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}) records, err := p.dohLookup(context.Background(), proxyQuery, GoogleProvider) r.NoError(t, err) @@ -165,7 +173,7 @@ func TestProxyProvider_DoHLookup_Google(t *testing.T) { func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) { skipIfProxyIsSet(t) - p := newProxyProvider(Config{}, []string{Quad9Provider, GoogleProvider}, proxyQuery) + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}) url, err := p.findReachableServer() r.NoError(t, err) @@ -175,18 +183,9 @@ func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) { func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T) { skipIfProxyIsSet(t) - p := newProxyProvider(Config{}, []string{"https://unreachable", Quad9Provider, GoogleProvider}, proxyQuery) + p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"https://unreachable", Quad9Provider, GoogleProvider}) url, err := p.findReachableServer() r.NoError(t, err) r.NotEmpty(t, url) } - -// skipIfProxyIsSet skips the tests if HTTPS proxy is set. -// Should be used for tests depending on proper certificate checks which -// is not possible under our CI setup. -func skipIfProxyIsSet(t *testing.T) { - if httpproxy.FromEnvironment().HTTPSProxy != "" { - t.SkipNow() - } -} diff --git a/pkg/pmapi/dialer_proxy_test.go b/internal/dialer/dialer_proxy_test.go similarity index 84% rename from pkg/pmapi/dialer_proxy_test.go rename to internal/dialer/dialer_proxy_test.go index 7ac2a459..f6963f9c 100644 --- a/pkg/pmapi/dialer_proxy_test.go +++ b/internal/dialer/dialer_proxy_test.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package pmapi +package dialer import ( "context" @@ -141,9 +141,10 @@ func TestProxyDialer_UseProxy(t *testing.T) { trustedProxy := getTrustedServer() defer closeServer(trustedProxy) - cfg := Config{HostURL: ""} - d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg)) - d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil } + provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders) + d := NewProxyTLSDialer(NewBasicTLSDialer(""), "") + d.proxyProvider = provider + provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil } err := d.switchToReachableServer() require.NoError(t, err) @@ -158,9 +159,10 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) { proxy3 := getTrustedServer() defer closeServer(proxy3) - cfg := Config{HostURL: ""} - d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg)) - d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL}, nil } + provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders) + d := NewProxyTLSDialer(NewBasicTLSDialer(""), "") + d.proxyProvider = provider + provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL}, nil } err := d.switchToReachableServer() require.NoError(t, err) @@ -169,7 +171,7 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) { // Have to wait so as to not get rejected. time.Sleep(proxyLookupWait) - d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy2.URL}, nil } + provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy2.URL}, nil } err = d.switchToReachableServer() require.NoError(t, err) require.Equal(t, formatAsAddress(proxy2.URL), d.proxyAddress) @@ -177,7 +179,7 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) { // Have to wait so as to not get rejected. time.Sleep(proxyLookupWait) - d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy3.URL}, nil } + provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy3.URL}, nil } err = d.switchToReachableServer() require.NoError(t, err) require.Equal(t, formatAsAddress(proxy3.URL), d.proxyAddress) @@ -187,11 +189,12 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) { trustedProxy := getTrustedServer() defer closeServer(trustedProxy) - cfg := Config{HostURL: ""} - d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg)) + provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders) + d := NewProxyTLSDialer(NewBasicTLSDialer(""), "") + d.proxyProvider = provider d.proxyUseDuration = time.Second - d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil } + provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil } err := d.switchToReachableServer() require.NoError(t, err) require.Equal(t, formatAsAddress(trustedProxy.URL), d.proxyAddress) @@ -203,9 +206,10 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) { func TestProxyDialer_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable(t *testing.T) { trustedProxy := getTrustedServer() - cfg := Config{HostURL: ""} - d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg)) - d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil } + provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders) + d := NewProxyTLSDialer(NewBasicTLSDialer(""), "") + d.proxyProvider = provider + provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil } err := d.switchToReachableServer() require.NoError(t, err) @@ -214,7 +218,7 @@ func TestProxyDialer_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable // Simulate that the proxy stops working and that the standard api is reachable again. closeServer(trustedProxy) d.directAddress = formatAsAddress(getRootURL()) - d.proxyProvider.cfg.HostURL = getRootURL() + provider.hostURL = getRootURL() time.Sleep(proxyLookupWait) // We should now find the original API URL if it is working again. @@ -232,9 +236,10 @@ func TestProxyDialer_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBloc proxy2 := getTrustedServer() defer closeServer(proxy2) - cfg := Config{HostURL: ""} - d := NewProxyTLSDialer(cfg, NewBasicTLSDialer(cfg)) - d.proxyProvider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil } + provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders) + d := NewProxyTLSDialer(NewBasicTLSDialer(""), "") + d.proxyProvider = provider + provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil } err := d.switchToReachableServer() require.NoError(t, err) diff --git a/internal/dialer/dialer_test.go b/internal/dialer/dialer_test.go new file mode 100644 index 00000000..044066a8 --- /dev/null +++ b/internal/dialer/dialer_test.go @@ -0,0 +1,16 @@ +package dialer + +import ( + "testing" + + "golang.org/x/net/http/httpproxy" +) + +// skipIfProxyIsSet skips the tests if HTTPS proxy is set. +// Should be used for tests depending on proper certificate checks which +// is not possible under our CI setup. +func skipIfProxyIsSet(t *testing.T) { + if httpproxy.FromEnvironment().HTTPSProxy != "" { + t.SkipNow() + } +} diff --git a/internal/events/connection.go b/internal/events/connection.go new file mode 100644 index 00000000..c3e456c3 --- /dev/null +++ b/internal/events/connection.go @@ -0,0 +1,13 @@ +package events + +import "gitlab.protontech.ch/go/liteapi" + +type TLSIssue struct { + eventBase +} + +type ConnStatus struct { + eventBase + + Status liteapi.Status +} diff --git a/internal/events/error.go b/internal/events/error.go new file mode 100644 index 00000000..b5b720a8 --- /dev/null +++ b/internal/events/error.go @@ -0,0 +1,7 @@ +package events + +type Error struct { + eventBase + + Error error +} diff --git a/internal/events/events.go b/internal/events/events.go index f2a73a3a..1fb3fd27 100644 --- a/internal/events/events.go +++ b/internal/events/events.go @@ -1,60 +1,9 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package events provides names of events used by the event listener in bridge. package events -import ( - "time" - - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" -) - -// Constants of events used by the event listener in bridge. -const ( - ErrorEvent = "error" - CredentialsErrorEvent = "credentialsError" - CloseConnectionEvent = "closeConnection" - LogoutEvent = "logout" - AddressChangedEvent = "addressChanged" - AddressChangedLogoutEvent = "addressChangedLogout" - UserRefreshEvent = "userRefresh" - RestartBridgeEvent = "restartBridge" - InternetConnChangedEvent = "internetChanged" - InternetOff = "internetOff" - InternetOn = "internetOn" - SecondInstanceEvent = "secondInstance" - NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient" - UpgradeApplicationEvent = "upgradeApplication" - TLSCertIssue = "tlsCertPinningIssue" - UserChangeDone = "QMLUserChangedDone" - - // LogoutEventTimeout is the minimum time to permit between logout events being sent. - LogoutEventTimeout = 3 * time.Minute -) - -// SetupEvents specific to event type and data. -func SetupEvents(listener listener.Listener) { - listener.SetLimit(LogoutEvent, LogoutEventTimeout) - listener.SetBuffer(ErrorEvent) - listener.SetBuffer(CredentialsErrorEvent) - listener.SetBuffer(InternetConnChangedEvent) - listener.SetBuffer(UpgradeApplicationEvent) - listener.SetBuffer(TLSCertIssue) - listener.SetBuffer(UserRefreshEvent) - listener.Book(UserChangeDone) +type Event interface { + _isEvent() } + +type eventBase struct{} + +func (eventBase) _isEvent() {} diff --git a/internal/events/raise.go b/internal/events/raise.go new file mode 100644 index 00000000..a8a11244 --- /dev/null +++ b/internal/events/raise.go @@ -0,0 +1,5 @@ +package events + +type Raise struct { + eventBase +} diff --git a/internal/events/sync.go b/internal/events/sync.go new file mode 100644 index 00000000..14413c65 --- /dev/null +++ b/internal/events/sync.go @@ -0,0 +1,24 @@ +package events + +import "time" + +type SyncStarted struct { + eventBase + + UserID string +} + +type SyncProgress struct { + eventBase + + UserID string + Progress float64 + Elapsed time.Duration + Remaining time.Duration +} + +type SyncFinished struct { + eventBase + + UserID string +} diff --git a/internal/events/update.go b/internal/events/update.go new file mode 100644 index 00000000..fe0aff64 --- /dev/null +++ b/internal/events/update.go @@ -0,0 +1,25 @@ +package events + +import "github.com/ProtonMail/proton-bridge/v2/internal/updater" + +type UpdateAvailable struct { + eventBase + + Version updater.VersionInfo + + CanInstall bool +} + +type UpdateNotAvailable struct { + eventBase +} + +type UpdateInstalled struct { + eventBase + + Version updater.VersionInfo +} + +type UpdateForced struct { + eventBase +} diff --git a/internal/events/user.go b/internal/events/user.go new file mode 100644 index 00000000..7b6a525d --- /dev/null +++ b/internal/events/user.go @@ -0,0 +1,52 @@ +package events + +type UserLoggedIn struct { + eventBase + + UserID string +} + +type UserLoggedOut struct { + eventBase + + UserID string +} + +type UserDeauth struct { + eventBase + + UserID string +} + +type UserDeleted struct { + eventBase + + UserID string +} + +type UserChanged struct { + eventBase + + UserID string +} + +type UserAddressCreated struct { + eventBase + + UserID string + Address string +} + +type UserAddressChanged struct { + eventBase + + UserID string + Address string +} + +type UserAddressDeleted struct { + eventBase + + UserID string + Address string +} diff --git a/internal/focus/client.go b/internal/focus/client.go new file mode 100644 index 00000000..982c427c --- /dev/null +++ b/internal/focus/client.go @@ -0,0 +1,32 @@ +package focus + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/ProtonMail/proton-bridge/v2/internal/focus/proto" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TryRaise() bool { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + cc, err := grpc.DialContext(ctx, net.JoinHostPort(Host, fmt.Sprint(Port)), grpc.WithInsecure()) + if err != nil { + return false + } + + if _, err := proto.NewFocusClient(cc).Raise(ctx, &emptypb.Empty{}); err != nil { + return false + } + + if err := cc.Close(); err != nil { + return false + } + + return true +} diff --git a/internal/focus/focus_test.go b/internal/focus/focus_test.go new file mode 100644 index 00000000..ce5e4beb --- /dev/null +++ b/internal/focus/focus_test.go @@ -0,0 +1,25 @@ +package focus + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFocusRaise(t *testing.T) { + // Start the focus service. + service, err := NewService() + require.NoError(t, err) + + // Try to dial it, it should succeed. + require.True(t, TryRaise()) + + // The service should report a raise call. + <-service.GetRaiseCh() + + // Stop the service. + service.Close() + + // Try to dial it, it should fail. + require.False(t, TryRaise()) +} diff --git a/internal/focus/proto/focus.go b/internal/focus/proto/focus.go new file mode 100644 index 00000000..696fefae --- /dev/null +++ b/internal/focus/proto/focus.go @@ -0,0 +1,3 @@ +package proto + +//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative focus.proto diff --git a/internal/focus/proto/focus.pb.go b/internal/focus/proto/focus.pb.go new file mode 100644 index 00000000..ad511512 --- /dev/null +++ b/internal/focus/proto/focus.pb.go @@ -0,0 +1,93 @@ +// Copyright (c) 2022 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.21.6 +// source: focus.proto + +package proto + +import ( + reflect "reflect" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +var File_focus_proto protoreflect.FileDescriptor + +var file_focus_proto_rawDesc = []byte{ + 0x0a, 0x0b, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x32, 0x40, 0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x52, 0x61, + 0x69, 0x73, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var file_focus_proto_goTypes = []interface{}{ + (*emptypb.Empty)(nil), // 0: google.protobuf.Empty +} +var file_focus_proto_depIdxs = []int32{ + 0, // 0: proto.Focus.Raise:input_type -> google.protobuf.Empty + 0, // 1: proto.Focus.Raise:output_type -> google.protobuf.Empty + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_focus_proto_init() } +func file_focus_proto_init() { + if File_focus_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_focus_proto_rawDesc, + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_focus_proto_goTypes, + DependencyIndexes: file_focus_proto_depIdxs, + }.Build() + File_focus_proto = out.File + file_focus_proto_rawDesc = nil + file_focus_proto_goTypes = nil + file_focus_proto_depIdxs = nil +} diff --git a/internal/focus/proto/focus.proto b/internal/focus/proto/focus.proto new file mode 100644 index 00000000..9691d8d3 --- /dev/null +++ b/internal/focus/proto/focus.proto @@ -0,0 +1,31 @@ +// Copyright (c) 2022 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +syntax = "proto3"; + +import "google/protobuf/empty.proto"; + +option go_package = "github.com/ProtonMail/proton-bridge/v2/internal/focus/proto"; + +package proto; + +//********************************************************************************************************************** +// Service Declaration +//**********************************************************************************************************************≠–– +service Focus { + rpc Raise(google.protobuf.Empty) returns (google.protobuf.Empty); +} diff --git a/internal/focus/proto/focus_grpc.pb.go b/internal/focus/proto/focus_grpc.pb.go new file mode 100644 index 00000000..3dcbc6d7 --- /dev/null +++ b/internal/focus/proto/focus_grpc.pb.go @@ -0,0 +1,107 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.21.6 +// source: focus.proto + +package proto + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// FocusClient is the client API for Focus service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type FocusClient interface { + Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type focusClient struct { + cc grpc.ClientConnInterface +} + +func NewFocusClient(cc grpc.ClientConnInterface) FocusClient { + return &focusClient{cc} +} + +func (c *focusClient) Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.Focus/Raise", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// FocusServer is the server API for Focus service. +// All implementations must embed UnimplementedFocusServer +// for forward compatibility +type FocusServer interface { + Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + mustEmbedUnimplementedFocusServer() +} + +// UnimplementedFocusServer must be embedded to have forward compatible implementations. +type UnimplementedFocusServer struct { +} + +func (UnimplementedFocusServer) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Raise not implemented") +} +func (UnimplementedFocusServer) mustEmbedUnimplementedFocusServer() {} + +// UnsafeFocusServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to FocusServer will +// result in compilation errors. +type UnsafeFocusServer interface { + mustEmbedUnimplementedFocusServer() +} + +func RegisterFocusServer(s grpc.ServiceRegistrar, srv FocusServer) { + s.RegisterService(&Focus_ServiceDesc, srv) +} + +func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(FocusServer).Raise(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Focus/Raise", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FocusServer).Raise(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// Focus_ServiceDesc is the grpc.ServiceDesc for Focus service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Focus_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.Focus", + HandlerType: (*FocusServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Raise", + Handler: _Focus_Raise_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "focus.proto", +} diff --git a/internal/focus/service.go b/internal/focus/service.go new file mode 100644 index 00000000..18be2f10 --- /dev/null +++ b/internal/focus/service.go @@ -0,0 +1,60 @@ +package focus + +import ( + "context" + "fmt" + "net" + + "github.com/ProtonMail/proton-bridge/v2/internal/focus/proto" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" +) + +const ( + Host = "127.0.0.1" + Port = 1042 +) + +type FocusService struct { + proto.UnimplementedFocusServer + + server *grpc.Server + listener net.Listener + raiseCh chan struct{} +} + +func NewService() (*FocusService, error) { + listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(Port))) + if err != nil { + return nil, fmt.Errorf("failed to listen: %w", err) + } + + service := &FocusService{ + server: grpc.NewServer(), + listener: listener, + raiseCh: make(chan struct{}, 1), + } + + proto.RegisterFocusServer(service.server, service) + + go func() { + if err := service.server.Serve(listener); err != nil { + fmt.Printf("failed to serve: %v", err) + } + }() + + return service, nil +} + +func (service *FocusService) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + service.raiseCh <- struct{}{} + return &emptypb.Empty{}, nil +} + +func (service *FocusService) GetRaiseCh() <-chan struct{} { + return service.raiseCh +} + +func (service *FocusService) Close() { + service.server.Stop() +} diff --git a/internal/frontend/cli/account_utils.go b/internal/frontend/cli/account_utils.go index 8f04e644..78f0e8e5 100644 --- a/internal/frontend/cli/account_utils.go +++ b/internal/frontend/cli/account_utils.go @@ -22,7 +22,7 @@ import ( "strconv" "strings" - "github.com/ProtonMail/proton-bridge/v2/internal/users" + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/abiosoft/ishell" ) @@ -35,6 +35,7 @@ func (f *frontendCLI) completeUsernames(args []string) (usernames []string) { if len(args) == 1 { arg = args[0] } + for _, userID := range f.bridge.GetUserIDs() { user, err := f.bridge.GetUserInfo(userID) if err != nil { @@ -50,8 +51,7 @@ func (f *frontendCLI) completeUsernames(args []string) (usernames []string) { // noAccountWrapper is a decorator for functions which need any account to be properly functional. func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ishell.Context) { return func(c *ishell.Context) { - users := f.bridge.GetUserIDs() - if len(users) == 0 { + if len(f.bridge.GetUserIDs()) == 0 { f.Println("No active accounts. Please add account to continue.") } else { callback(c) @@ -59,9 +59,9 @@ func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ish } } -func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) users.UserInfo { +func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) bridge.UserInfo { user := f.getUserByIndexOrName("") - if user.ID != "" { + if user.UserID != "" { return user } @@ -69,24 +69,24 @@ func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) users.UserInfo { indexRange := fmt.Sprintf("number between 0 and %d", numberOfAccounts-1) if len(c.Args) == 0 { f.Printf("Please choose %s or username.\n", indexRange) - return users.UserInfo{} + return bridge.UserInfo{} } arg := c.Args[0] user = f.getUserByIndexOrName(arg) - if user.ID == "" { + if user.UserID == "" { f.Printf("Wrong input '%s'. Choose %s or username.\n", bold(arg), indexRange) - return users.UserInfo{} + return bridge.UserInfo{} } return user } -func (f *frontendCLI) getUserByIndexOrName(arg string) users.UserInfo { +func (f *frontendCLI) getUserByIndexOrName(arg string) bridge.UserInfo { userIDs := f.bridge.GetUserIDs() numberOfAccounts := len(userIDs) if numberOfAccounts == 0 { - return users.UserInfo{} + return bridge.UserInfo{} } - res := make([]users.UserInfo, len(userIDs)) + res := make([]bridge.UserInfo, len(userIDs)) for idx, userID := range userIDs { user, err := f.bridge.GetUserInfo(userID) if err != nil { @@ -99,7 +99,7 @@ func (f *frontendCLI) getUserByIndexOrName(arg string) users.UserInfo { } if index, err := strconv.Atoi(arg); err == nil { if index < 0 || index >= numberOfAccounts { - return users.UserInfo{} + return bridge.UserInfo{} } return res[index] } @@ -108,5 +108,5 @@ func (f *frontendCLI) getUserByIndexOrName(arg string) users.UserInfo { return user } } - return users.UserInfo{} + return bridge.UserInfo{} } diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go index 71960de3..e3ce1f81 100644 --- a/internal/frontend/cli/accounts.go +++ b/internal/frontend/cli/accounts.go @@ -22,8 +22,7 @@ import ( "strings" "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/users" + "github.com/ProtonMail/proton-bridge/v2/internal/constants" "github.com/abiosoft/ishell" ) @@ -40,7 +39,7 @@ func (f *frontendCLI) listAccounts(c *ishell.Context) { connected = "connected" } mode := "split" - if user.Mode == users.CombinedMode { + if user.AddressMode == bridge.CombinedMode { mode = "combined" } f.Printf(spacing, idx, user.Username, connected, mode) @@ -50,7 +49,7 @@ func (f *frontendCLI) listAccounts(c *ishell.Context) { func (f *frontendCLI) showAccountInfo(c *ishell.Context) { user := f.askUserByIndexOrName(c) - if user.ID == "" { + if user.UserID == "" { return } @@ -59,8 +58,8 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) { return } - if user.Mode == users.CombinedMode { - f.showAccountAddressInfo(user, user.Addresses[user.Primary]) + if user.AddressMode == bridge.CombinedMode { + f.showAccountAddressInfo(user, user.Addresses[0]) } else { for _, address := range user.Addresses { f.showAccountAddressInfo(user, address) @@ -68,25 +67,31 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) { } } -func (f *frontendCLI) showAccountAddressInfo(user users.UserInfo, address string) { +func (f *frontendCLI) showAccountAddressInfo(user bridge.UserInfo, address string) { + imapSecurity := "STARTTLS" + if f.bridge.GetIMAPSSL() { + imapSecurity = "SSL" + } + smtpSecurity := "STARTTLS" - if f.bridge.GetBool(settings.SMTPSSLKey) { + if f.bridge.GetSMTPSSL() { smtpSecurity = "SSL" } + f.Println(bold("Configuration for " + address)) f.Printf("IMAP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n", - bridge.Host, - f.bridge.GetInt(settings.IMAPPortKey), + constants.Host, + f.bridge.GetIMAPPort(), address, - user.Password, - "STARTTLS", + user.BridgePass, + imapSecurity, ) f.Println("") f.Printf("SMTP Settings\nAddress: %s\nSMTP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n", - bridge.Host, - f.bridge.GetInt(settings.SMTPPortKey), + constants.Host, + f.bridge.GetSMTPPort(), address, - user.Password, + user.BridgePass, smtpSecurity, ) f.Println("") @@ -99,8 +104,8 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen loginName := "" if len(c.Args) > 0 { user := f.getUserByIndexOrName(c.Args[0]) - if user.ID != "" { - loginName = user.Addresses[user.Primary] + if user.UserID != "" { + loginName = user.Addresses[0] } } @@ -119,41 +124,23 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen } f.Println("Authenticating ... ") - client, auth, err := f.bridge.Login(loginName, []byte(password)) + + userID, err := f.bridge.LoginUser( + context.Background(), + loginName, + password, + func() (string, error) { + return f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty), nil + }, + func() ([]byte, error) { + return []byte(f.readStringInAttempts("Mailbox password", c.ReadPassword, isNotEmpty)), nil + }, + ) if err != nil { f.processAPIError(err) return } - if auth.HasTwoFactor() { - twoFactor := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty) - if twoFactor == "" { - return - } - - err = client.Auth2FA(context.Background(), twoFactor) - if err != nil { - f.processAPIError(err) - return - } - } - - mailboxPassword := password - if auth.HasMailboxPassword() { - mailboxPassword = f.readStringInAttempts("Mailbox password", c.ReadPassword, isNotEmpty) - } - if mailboxPassword == "" { - return - } - - f.Println("Adding account ...") - userID, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword)) - if err != nil { - log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful") - f.Println("Adding account was unsuccessful:", err) - return - } - user, err := f.bridge.GetUserInfo(userID) if err != nil { panic(err) @@ -167,11 +154,12 @@ func (f *frontendCLI) logoutAccount(c *ishell.Context) { defer f.ShowPrompt(true) user := f.askUserByIndexOrName(c) - if user.ID == "" { + if user.UserID == "" { return } + if f.yesNoQuestion("Are you sure you want to logout account " + bold(user.Username)) { - if err := f.bridge.LogoutUser(user.ID); err != nil { + if err := f.bridge.LogoutUser(context.Background(), user.UserID); err != nil { f.printAndLogError("Logging out failed: ", err) } } @@ -182,12 +170,12 @@ func (f *frontendCLI) deleteAccount(c *ishell.Context) { defer f.ShowPrompt(true) user := f.askUserByIndexOrName(c) - if user.ID == "" { + if user.UserID == "" { return } + if f.yesNoQuestion("Are you sure you want to " + bold("remove account "+user.Username)) { - clearCache := f.yesNoQuestion("Do you want to remove cache for this account") - if err := f.bridge.DeleteUser(user.ID, clearCache); err != nil { + if err := f.bridge.DeleteUser(context.Background(), user.UserID); err != nil { f.printAndLogError("Cannot delete account: ", err) return } @@ -205,10 +193,13 @@ func (f *frontendCLI) deleteAccounts(c *ishell.Context) { for _, userID := range f.bridge.GetUserIDs() { user, err := f.bridge.GetUserInfo(userID) if err != nil { - panic(err) + f.printAndLogError("Cannot get user info: ", err) + return } - if err := f.bridge.DeleteUser(user.ID, false); err != nil { + + if err := f.bridge.DeleteUser(context.Background(), user.UserID); err != nil { f.printAndLogError("Cannot delete account ", user.Username, ": ", err) + return } } @@ -223,37 +214,50 @@ func (f *frontendCLI) deleteEverything(c *ishell.Context) { return } - f.bridge.FactoryReset() + f.bridge.FactoryReset(context.Background()) c.Println("Everything cleared") - - // Clearing data removes everything (db, preferences, ...) so everything has to be stopped and started again. - f.restarter.SetToRestart() - - f.Stop() } func (f *frontendCLI) changeMode(c *ishell.Context) { user := f.askUserByIndexOrName(c) - if user.ID == "" { + if user.UserID == "" { return } - var targetMode users.AddressMode + var targetMode bridge.AddressMode - if user.Mode == users.CombinedMode { - targetMode = users.SplitMode + if user.AddressMode == bridge.CombinedMode { + targetMode = bridge.SplitMode } else { - targetMode = users.CombinedMode + targetMode = bridge.CombinedMode } if !f.yesNoQuestion("Are you sure you want to change the mode for account " + bold(user.Username) + " to " + bold(targetMode)) { return } - if err := f.bridge.SetAddressMode(user.ID, targetMode); err != nil { + if err := f.bridge.SetAddressMode(user.UserID, targetMode); err != nil { f.printAndLogError("Cannot switch address mode:", err) } f.Printf("Address mode for account %s changed to %s\n", user.Username, targetMode) } + +func (f *frontendCLI) configureAppleMail(c *ishell.Context) { + user := f.askUserByIndexOrName(c) + if user.UserID == "" { + return + } + + if !f.yesNoQuestion("Are you sure you want to configure Apple Mail for " + bold(user.Username) + " with address " + bold(user.Addresses[0])) { + return + } + + if err := f.bridge.ConfigureAppleMail(user.UserID, user.Addresses[0]); err != nil { + f.printAndLogError(err) + return + } + + f.Printf("Apple Mail configured for %v with address %v\n", user.Username, user.Addresses[0]) +} diff --git a/internal/frontend/cli/frontend.go b/internal/frontend/cli/frontend.go index 10ad573c..0d4384ab 100644 --- a/internal/frontend/cli/frontend.go +++ b/internal/frontend/cli/frontend.go @@ -19,11 +19,13 @@ package cli import ( + "errors" + + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/ProtonMail/proton-bridge/v2/internal/constants" "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" - "github.com/ProtonMail/proton-bridge/v2/internal/updater" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" + "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "gitlab.protontech.ch/go/liteapi" "github.com/abiosoft/ishell" "github.com/sirupsen/logrus" @@ -34,30 +36,14 @@ var log = logrus.WithField("pkg", "frontend/cli") //nolint:gochecknoglobals type frontendCLI struct { *ishell.Shell - eventListener listener.Listener - updater types.Updater - bridge types.Bridger - - restarter types.Restarter + bridge *bridge.Bridge } // New returns a new CLI frontend configured with the given options. -func New( //nolint:funlen - panicHandler types.PanicHandler, - - eventListener listener.Listener, - updater types.Updater, - bridge types.Bridger, - restarter types.Restarter, -) *frontendCLI { //nolint:revive +func New(bridge *bridge.Bridge) *frontendCLI { fe := &frontendCLI{ - Shell: ishell.New(), - - eventListener: eventListener, - updater: updater, - bridge: bridge, - - restarter: restarter, + Shell: ishell.New(), + bridge: bridge, } // Clear commands. @@ -66,12 +52,6 @@ func New( //nolint:funlen Help: "remove stored accounts and preferences. (alias: cl)", Aliases: []string{"cl"}, } - clearCmd.AddCmd(&ishell.Cmd{ - Name: "cache", - Help: "remove stored preferences for accounts (aliases: c, prefs, preferences)", - Aliases: []string{"c", "prefs", "preferences"}, - Func: fe.deleteCache, - }) clearCmd.AddCmd(&ishell.Cmd{ Name: "accounts", Help: "remove all accounts from keychain. (aliases: a, k, keychain)", @@ -100,15 +80,30 @@ func New( //nolint:funlen Completer: fe.completeUsernames, }) changeCmd.AddCmd(&ishell.Cmd{ - Name: "port", - Help: "change port numbers of IMAP and SMTP servers. (alias: p)", - Aliases: []string{"p"}, - Func: fe.changePort, + Name: "change-location", + Help: "change the location of the encrypted message cache", + Func: fe.setGluonLocation, + }) + changeCmd.AddCmd(&ishell.Cmd{ + Name: "imap-port", + Help: "change port number of IMAP server.", + Func: fe.changeIMAPPort, + }) + changeCmd.AddCmd(&ishell.Cmd{ + Name: "smtp-port", + Help: "change port number of SMTP server.", + Func: fe.changeSMTPPort, + }) + changeCmd.AddCmd(&ishell.Cmd{ + Name: "imap-security", + Help: "change IMAP SSL settings servers.(alias: ssl-imap, starttls-imap)", + Aliases: []string{"ssl-imap", "starttls-imap"}, + Func: fe.changeIMAPSecurity, }) changeCmd.AddCmd(&ishell.Cmd{ Name: "smtp-security", - Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)", - Aliases: []string{"ssl", "starttls"}, + Help: "change SMTP SSL settings servers.(alias: ssl-smtp, starttls-smtp)", + Aliases: []string{"ssl-smtp", "starttls-smtp"}, Func: fe.changeSMTPSecurity, }) fe.AddCmd(changeCmd) @@ -130,6 +125,22 @@ func New( //nolint:funlen }) fe.AddCmd(dohCmd) + // Apple Mail commands. + configureCmd := &ishell.Cmd{ + Name: "configure-apple-mail", + Help: "Configures Apple Mail to use ProtonMail Bridge", + Func: fe.configureAppleMail, + } + fe.AddCmd(configureCmd) + + // TLS commands. + exportTLSCmd := &ishell.Cmd{ + Name: "export-tls", + Help: "Export the TLS certificate used by the Bridge", + Func: fe.exportTLSCerts, + } + fe.AddCmd(exportTLSCmd) + // All mail visibility commands. allMailCmd := &ishell.Cmd{ Name: "all-mail-visibility", @@ -147,28 +158,6 @@ func New( //nolint:funlen }) fe.AddCmd(allMailCmd) - // Cache-On-Disk commands. - codCmd := &ishell.Cmd{ - Name: "local-cache", - Help: "manage the local encrypted message cache", - } - codCmd.AddCmd(&ishell.Cmd{ - Name: "enable", - Help: "enable the local cache", - Func: fe.enableCacheOnDisk, - }) - codCmd.AddCmd(&ishell.Cmd{ - Name: "disable", - Help: "disable the local cache", - Func: fe.disableCacheOnDisk, - }) - codCmd.AddCmd(&ishell.Cmd{ - Name: "change-location", - Help: "change the location of the local cache", - Func: fe.setCacheOnDiskLocation, - }) - fe.AddCmd(codCmd) - // Updates commands. updatesCmd := &ishell.Cmd{ Name: "updates", @@ -224,7 +213,6 @@ func New( //nolint:funlen Aliases: []string{"man"}, Func: fe.printManual, }) - fe.AddCmd(&ishell.Cmd{ Name: "credits", Help: "print used resources.", @@ -267,55 +255,122 @@ func New( //nolint:funlen Completer: fe.completeUsernames, }) - // System commands. - fe.AddCmd(&ishell.Cmd{ - Name: "restart", - Help: "restart the bridge.", - Func: fe.restart, - }) + go fe.watchEvents() - go func() { - defer panicHandler.HandlePanic() - fe.watchEvents() - }() return fe } func (f *frontendCLI) watchEvents() { - errorCh := f.eventListener.ProvideChannel(events.ErrorEvent) - credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent) - internetConnChangedCh := f.eventListener.ProvideChannel(events.InternetConnChangedEvent) - addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent) - addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent) - logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent) - certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue) - for { - select { - case errorDetails := <-errorCh: - f.Println("Bridge failed:", errorDetails) - case <-credentialsErrorCh: + eventCh, done := f.bridge.GetEvents() + defer done() + + // TODO: Better error events. + for _, err := range f.bridge.GetErrors() { + switch { + case errors.Is(err, vault.ErrCorrupt): f.notifyCredentialsError() - case stat := <-internetConnChangedCh: - if stat == events.InternetOff { + + case errors.Is(err, vault.ErrInsecure): + f.notifyCredentialsError() + + case errors.Is(err, bridge.ErrServeIMAP): + f.Println("IMAP server error:", err) + + case errors.Is(err, bridge.ErrServeSMTP): + f.Println("SMTP server error:", err) + } + } + + for event := range eventCh { + switch event := event.(type) { + case events.ConnStatus: + switch event.Status { + case liteapi.StatusUp: + f.notifyInternetOn() + + case liteapi.StatusDown: f.notifyInternetOff() } - if stat == events.InternetOn { - f.notifyInternetOn() - } - case address := <-addressChangedCh: - f.Printf("Address changed for %s. You may need to reconfigure your email client.", address) - case address := <-addressChangedLogoutCh: - f.notifyLogout(address) - case userID := <-logoutCh: - user, err := f.bridge.GetUserInfo(userID) + + case events.UserDeauth: + user, err := f.bridge.GetUserInfo(event.UserID) if err != nil { return } + f.notifyLogout(user.Username) - case <-certIssue: + + case events.UserAddressChanged: + user, err := f.bridge.GetUserInfo(event.UserID) + if err != nil { + return + } + + f.Printf("Address changed for %s. You may need to reconfigure your email client.\n", user.Username) + + case events.UserAddressDeleted: + f.notifyLogout(event.Address) + + case events.SyncStarted: + user, err := f.bridge.GetUserInfo(event.UserID) + if err != nil { + return + } + + f.Printf("A sync has begun for %s.\n", user.Username) + + case events.SyncFinished: + user, err := f.bridge.GetUserInfo(event.UserID) + if err != nil { + return + } + + f.Printf("A sync has finished for %s.\n", user.Username) + + case events.SyncProgress: + user, err := f.bridge.GetUserInfo(event.UserID) + if err != nil { + return + } + + f.Printf( + "Sync (%v): %.1f%% (Elapsed: %0.1fs, ETA: %0.1fs)\n", + user.Username, + 100*event.Progress, + event.Elapsed.Seconds(), + event.Remaining.Seconds(), + ) + + case events.UpdateAvailable: + f.Printf("An update is available (version %v)\n", event.Version.Version) + + case events.UpdateForced: + f.notifyNeedUpgrade() + + case events.TLSIssue: f.notifyCertIssue() } } + + /* + errorCh := f.eventListener.ProvideChannel(events.ErrorEvent) + credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent) + for { + select { + case errorDetails := <-errorCh: + f.Println("Bridge failed:", errorDetails) + case <-credentialsErrorCh: + f.notifyCredentialsError() + case stat := <-internetConnChangedCh: + if stat == events.InternetOff { + f.notifyInternetOff() + } + if stat == events.InternetOn { + f.notifyInternetOn() + } + } + } + */ } // Loop starts the frontend loop with an interactive shell. @@ -340,12 +395,3 @@ func (f *frontendCLI) Loop() error { f.Run() return nil } - -func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) { - // NOTE: Save the update somewhere so that it can be installed when user chooses "install now". -} - -func (f *frontendCLI) WaitUntilFrontendIsReady() {} -func (f *frontendCLI) SetVersion(version updater.VersionInfo) {} -func (f *frontendCLI) NotifySilentUpdateInstalled() {} -func (f *frontendCLI) NotifySilentUpdateError(err error) {} diff --git a/internal/frontend/cli/system.go b/internal/frontend/cli/system.go index f770c9e3..f326e1ca 100644 --- a/internal/frontend/cli/system.go +++ b/internal/frontend/cli/system.go @@ -18,28 +18,21 @@ package cli import ( + "context" + "errors" "fmt" "os" + "path/filepath" "strconv" "strings" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/ProtonMail/proton-bridge/v2/pkg/ports" "github.com/abiosoft/ishell" ) -var currentPort = "" //nolint:gochecknoglobals - -func (f *frontendCLI) restart(c *ishell.Context) { - if f.yesNoQuestion("Are you sure you want to restart the Bridge") { - f.Println("Restarting Bridge...") - f.restarter.SetToRestart() - f.Stop() - } -} - func (f *frontendCLI) printLogDir(c *ishell.Context) { - if path, err := f.bridge.ProvideLogsPath(); err != nil { + if path, err := f.bridge.GetLogsPath(); err != nil { f.Println("Failed to determine location of log files") } else { f.Println("Log files are stored in\n\n ", path) @@ -50,79 +43,91 @@ func (f *frontendCLI) printManual(c *ishell.Context) { f.Println("More instructions about the Bridge can be found at\n\n https://protonmail.com/bridge") } -func (f *frontendCLI) deleteCache(c *ishell.Context) { +func (f *frontendCLI) printCredits(c *ishell.Context) { + for _, pkg := range strings.Split(bridge.Credits, ";") { + f.Println(pkg) + } +} + +func (f *frontendCLI) changeIMAPSecurity(c *ishell.Context) { f.ShowPrompt(false) defer f.ShowPrompt(true) - if !f.yesNoQuestion("Do you really want to remove all stored preferences") { - return + newSecurity := "SSL" + if f.bridge.GetIMAPSSL() { + newSecurity = "STARTTLS" } - if err := f.bridge.ClearData(); err != nil { - f.printAndLogError("Cache clear failed: ", err.Error()) - return + msg := fmt.Sprintf("Are you sure you want to change IMAP setting to %q", newSecurity) + + if f.yesNoQuestion(msg) { + if err := f.bridge.SetIMAPSSL(!f.bridge.GetIMAPSSL()); err != nil { + f.printAndLogError(err) + return + } } - - f.Println("Cached cleared, restarting bridge") - - // Clearing data removes everything (db, preferences, ...) so everything has to be stopped and started again. - f.restarter.SetToRestart() - - f.Stop() } func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) { f.ShowPrompt(false) defer f.ShowPrompt(true) - isSSL := f.bridge.GetBool(settings.SMTPSSLKey) newSecurity := "SSL" - if isSSL { + if f.bridge.GetSMTPSSL() { newSecurity = "STARTTLS" } - msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q and restart the Bridge", newSecurity) + msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q", newSecurity) if f.yesNoQuestion(msg) { - f.bridge.SetBool(settings.SMTPSSLKey, !isSSL) - f.Println("Restarting Bridge...") - f.restarter.SetToRestart() - f.Stop() + if err := f.bridge.SetSMTPSSL(!f.bridge.GetSMTPSSL()); err != nil { + f.printAndLogError(err) + return + } } } -func (f *frontendCLI) changePort(c *ishell.Context) { +func (f *frontendCLI) changeIMAPPort(c *ishell.Context) { f.ShowPrompt(false) defer f.ShowPrompt(true) - currentPort = f.bridge.Get(settings.IMAPPortKey) - newIMAPPort := f.readStringInAttempts("Set IMAP port (current "+currentPort+")", c.ReadLine, f.isPortFree) + newIMAPPort := f.readStringInAttempts(fmt.Sprintf("Set IMAP port (current %v)", f.bridge.GetIMAPPort()), c.ReadLine, f.isPortFree) if newIMAPPort == "" { - newIMAPPort = currentPort - } - imapPortChanged := newIMAPPort != currentPort - - currentPort = f.bridge.Get(settings.SMTPPortKey) - newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree) - if newSMTPPort == "" { - newSMTPPort = currentPort - } - smtpPortChanged := newSMTPPort != currentPort - - if newIMAPPort == newSMTPPort { - f.Println("SMTP and IMAP ports must be different!") + f.printAndLogError(errors.New("failed to get new port")) return } - if imapPortChanged || smtpPortChanged { - f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort) - f.bridge.Set(settings.IMAPPortKey, newIMAPPort) - f.bridge.Set(settings.SMTPPortKey, newSMTPPort) - f.Println("Restarting Bridge...") - f.restarter.SetToRestart() - f.Stop() - } else { - f.Println("Nothing changed") + newIMAPPortInt, err := strconv.Atoi(newIMAPPort) + if err != nil { + f.printAndLogError(err) + return + } + + if err := f.bridge.SetIMAPPort(newIMAPPortInt); err != nil { + f.printAndLogError(err) + return + } +} + +func (f *frontendCLI) changeSMTPPort(c *ishell.Context) { + f.ShowPrompt(false) + defer f.ShowPrompt(true) + + newSMTPPort := f.readStringInAttempts(fmt.Sprintf("Set SMTP port (current %v)", f.bridge.GetSMTPPort()), c.ReadLine, f.isPortFree) + if newSMTPPort == "" { + f.printAndLogError(errors.New("failed to get new port")) + return + } + + newSMTPPortInt, err := strconv.Atoi(newSMTPPort) + if err != nil { + f.printAndLogError(err) + return + } + + if err := f.bridge.SetSMTPPort(newSMTPPortInt); err != nil { + f.printAndLogError(err) + return } } @@ -135,7 +140,10 @@ func (f *frontendCLI) allowProxy(c *ishell.Context) { f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.") if f.yesNoQuestion("Are you sure you want to allow bridge to do this") { - f.bridge.SetProxyAllowed(true) + if err := f.bridge.SetProxyAllowed(true); err != nil { + f.printAndLogError(err) + return + } } } @@ -148,12 +156,15 @@ func (f *frontendCLI) disallowProxy(c *ishell.Context) { f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.") if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") { - f.bridge.SetProxyAllowed(false) + if err := f.bridge.SetProxyAllowed(false); err != nil { + f.printAndLogError(err) + return + } } } func (f *frontendCLI) hideAllMail(c *ishell.Context) { - if !f.bridge.IsAllMailVisible() { + if !f.bridge.GetShowAllMail() { f.Println("All Mail folder is not listed in your local client.") return } @@ -161,12 +172,15 @@ func (f *frontendCLI) hideAllMail(c *ishell.Context) { f.Println("All Mail folder is listed in your client right now.") if f.yesNoQuestion("Do you want to hide All Mail folder") { - f.bridge.SetIsAllMailVisible(false) + if err := f.bridge.SetShowAllMail(false); err != nil { + f.printAndLogError(err) + return + } } } func (f *frontendCLI) showAllMail(c *ishell.Context) { - if f.bridge.IsAllMailVisible() { + if f.bridge.GetShowAllMail() { f.Println("All Mail folder is listed in your local client.") return } @@ -174,68 +188,47 @@ func (f *frontendCLI) showAllMail(c *ishell.Context) { f.Println("All Mail folder is not listed in your client right now.") if f.yesNoQuestion("Do you want to show All Mail folder") { - f.bridge.SetIsAllMailVisible(true) + if err := f.bridge.SetShowAllMail(true); err != nil { + f.printAndLogError(err) + return + } } } -func (f *frontendCLI) enableCacheOnDisk(c *ishell.Context) { - if f.bridge.GetBool(settings.CacheEnabledKey) { - f.Println("The local cache is already enabled.") - return +func (f *frontendCLI) setGluonLocation(c *ishell.Context) { + if gluonDir := f.bridge.GetGluonDir(); gluonDir != "" { + f.Println("The current message cache location is:", gluonDir) } - if f.yesNoQuestion("Are you sure you want to enable the local cache") { - if err := f.bridge.EnableCache(); err != nil { - f.Println("The local cache could not be enabled.") + if location := f.readStringInAttempts("Enter a new location for the message cache", c.ReadLine, f.isCacheLocationUsable); location != "" { + if err := f.bridge.SetGluonDir(context.Background(), location); err != nil { + f.printAndLogError(err) return } - - f.restarter.SetToRestart() - f.Stop() } } -func (f *frontendCLI) disableCacheOnDisk(c *ishell.Context) { - if !f.bridge.GetBool(settings.CacheEnabledKey) { - f.Println("The local cache is already disabled.") - return - } +func (f *frontendCLI) exportTLSCerts(c *ishell.Context) { + if location := f.readStringInAttempts("Enter a path to which to export the TLS certificate used for IMAP and SMTP", c.ReadLine, f.isCacheLocationUsable); location != "" { + cert, key := f.bridge.GetBridgeTLSCert() - if f.yesNoQuestion("Are you sure you want to disable the local cache") { - if err := f.bridge.DisableCache(); err != nil { - f.Println("The local cache could not be disabled.") + if err := os.WriteFile(filepath.Join(location, "cert.pem"), cert, 0600); err != nil { + f.printAndLogError(err) return } - f.restarter.SetToRestart() - f.Stop() - } -} - -func (f *frontendCLI) setCacheOnDiskLocation(c *ishell.Context) { - if !f.bridge.GetBool(settings.CacheEnabledKey) { - f.Println("The local cache must be enabled.") - return - } - - if location := f.bridge.Get(settings.CacheLocationKey); location != "" { - f.Println("The current local cache location is:", location) - } - - if location := f.readStringInAttempts("Enter a new location for the cache", c.ReadLine, f.isCacheLocationUsable); location != "" { - if err := f.bridge.MigrateCache(f.bridge.Get(settings.CacheLocationKey), location); err != nil { - f.Println("The local cache location could not be changed.") + if err := os.WriteFile(filepath.Join(location, "key.pem"), key, 0600); err != nil { + f.printAndLogError(err) return } - f.restarter.SetToRestart() - f.Stop() + f.Println("TLS certificate exported to", location) } } func (f *frontendCLI) isPortFree(port string) bool { port = strings.ReplaceAll(port, ":", "") - if port == "" || port == currentPort { + if port == "" { return true } number, err := strconv.Atoi(port) diff --git a/internal/frontend/cli/updates.go b/internal/frontend/cli/updates.go index 3748849e..66206f24 100644 --- a/internal/frontend/cli/updates.go +++ b/internal/frontend/cli/updates.go @@ -18,36 +18,16 @@ package cli import ( - "strings" - - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" "github.com/ProtonMail/proton-bridge/v2/internal/updater" "github.com/abiosoft/ishell" ) func (f *frontendCLI) checkUpdates(c *ishell.Context) { - version, err := f.updater.Check() - if err != nil { - f.Println("An error occurred while checking for updates.") - return - } - - if f.updater.IsUpdateApplicable(version) { - f.Println("An update is available.") - } else { - f.Println("Your version is up to date.") - } -} - -func (f *frontendCLI) printCredits(c *ishell.Context) { - for _, pkg := range strings.Split(bridge.Credits, ";") { - f.Println(pkg) - } + f.bridge.CheckForUpdates() } func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) { - if f.bridge.GetBool(settings.AutoUpdateKey) { + if f.bridge.GetAutoUpdate() { f.Println("Bridge is already set to automatically install updates.") return } @@ -55,12 +35,15 @@ func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) { f.Println("Bridge is currently set to NOT automatically install updates.") if f.yesNoQuestion("Are you sure you want to allow bridge to do this") { - f.bridge.SetBool(settings.AutoUpdateKey, true) + if err := f.bridge.SetAutoUpdate(true); err != nil { + f.printAndLogError(err) + return + } } } func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) { - if !f.bridge.GetBool(settings.AutoUpdateKey) { + if !f.bridge.GetAutoUpdate() { f.Println("Bridge is already set to NOT automatically install updates.") return } @@ -68,7 +51,10 @@ func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) { f.Println("Bridge is currently set to automatically install updates.") if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") { - f.bridge.SetBool(settings.AutoUpdateKey, false) + if err := f.bridge.SetAutoUpdate(false); err != nil { + f.printAndLogError(err) + return + } } } @@ -81,7 +67,10 @@ func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) { f.Println("Bridge is currently on the stable update channel.") if f.yesNoQuestion("Are you sure you want to switch to the early-access update channel") { - f.bridge.SetUpdateChannel(updater.EarlyChannel) + if err := f.bridge.SetUpdateChannel(updater.EarlyChannel); err != nil { + f.printAndLogError(err) + return + } } } @@ -95,6 +84,9 @@ func (f *frontendCLI) selectStableChannel(c *ishell.Context) { f.Println("Switching to the stable channel may reset all data!") if f.yesNoQuestion("Are you sure you want to switch to the stable update channel") { - f.bridge.SetUpdateChannel(updater.StableChannel) + if err := f.bridge.SetUpdateChannel(updater.StableChannel); err != nil { + f.printAndLogError(err) + return + } } } diff --git a/internal/frontend/cli/utils.go b/internal/frontend/cli/utils.go index 84efbe9c..24621465 100644 --- a/internal/frontend/cli/utils.go +++ b/internal/frontend/cli/utils.go @@ -20,7 +20,6 @@ package cli import ( "strings" - pmapi "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" "github.com/fatih/color" ) @@ -67,15 +66,7 @@ func (f *frontendCLI) printAndLogError(args ...interface{}) { } func (f *frontendCLI) processAPIError(err error) { - log.Warn("API error: ", err) - switch err { - case pmapi.ErrNoConnection: - f.notifyInternetOff() - case pmapi.ErrUpgradeApplication: - f.notifyNeedUpgrade() - default: - f.Println("Server error:", err.Error()) - } + f.printAndLogError(err) } func (f *frontendCLI) notifyInternetOff() { @@ -91,12 +82,7 @@ func (f *frontendCLI) notifyLogout(address string) { } func (f *frontendCLI) notifyNeedUpgrade() { - version, err := f.updater.Check() - if err != nil { - log.WithError(err).Error("Failed to notify need upgrade") - return - } - f.Println("Please download and install the newest version of application from", version.LandingPage) + f.Println("Please download and install the newest version of the application.") } func (f *frontendCLI) notifyCredentialsError() { diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go deleted file mode 100644 index fc3e407b..00000000 --- a/internal/frontend/frontend.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package frontend provides all interfaces of the Bridge. -package frontend - -import ( - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/cli" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/grpc" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" - "github.com/ProtonMail/proton-bridge/v2/internal/locations" - "github.com/ProtonMail/proton-bridge/v2/internal/updater" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" -) - -type Frontend interface { - Loop() error - NotifyManualUpdate(update updater.VersionInfo, canInstall bool) - SetVersion(update updater.VersionInfo) - NotifySilentUpdateInstalled() - NotifySilentUpdateError(error) - WaitUntilFrontendIsReady() -} - -// New returns initialized frontend based on `frontendType`, which can be `cli` or `grpc`. -func New( - frontendType string, - showWindowOnStart bool, - panicHandler types.PanicHandler, - eventListener listener.Listener, - updater types.Updater, - bridge *bridge.Bridge, - restarter types.Restarter, - locations *locations.Locations, -) Frontend { - switch frontendType { - case "grpc": - return grpc.NewService( - showWindowOnStart, - panicHandler, - eventListener, - updater, - bridge, - restarter, - locations, - ) - - case "cli": - return cli.New( - panicHandler, - eventListener, - updater, - bridge, - restarter, - ) - - default: - return nil - } -} diff --git a/internal/frontend/grpc/bridge.pb.go b/internal/frontend/grpc/bridge.pb.go index 91405282..e2f81136 100644 --- a/internal/frontend/grpc/bridge.pb.go +++ b/internal/frontend/grpc/bridge.pb.go @@ -18,7 +18,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.0 -// protoc v3.21.3 +// protoc v3.21.7 // source: bridge.proto package grpc @@ -572,8 +572,7 @@ type ChangeLocalCacheRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - EnableDiskCache bool `protobuf:"varint,1,opt,name=enableDiskCache,proto3" json:"enableDiskCache,omitempty"` - DiskCachePath string `protobuf:"bytes,2,opt,name=diskCachePath,proto3" json:"diskCachePath,omitempty"` + DiskCachePath string `protobuf:"bytes,2,opt,name=diskCachePath,proto3" json:"diskCachePath,omitempty"` } func (x *ChangeLocalCacheRequest) Reset() { @@ -608,13 +607,6 @@ func (*ChangeLocalCacheRequest) Descriptor() ([]byte, []int) { return file_bridge_proto_rawDescGZIP(), []int{4} } -func (x *ChangeLocalCacheRequest) GetEnableDiskCache() bool { - if x != nil { - return x.EnableDiskCache - } - return false -} - func (x *ChangeLocalCacheRequest) GetDiskCachePath() string { if x != nil { return x.DiskCachePath @@ -3800,659 +3792,652 @@ var file_bridge_proto_rawDesc = []byte{ 0x77, 0x6f, 0x72, 0x64, 0x22, 0x2f, 0x0a, 0x11, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, - 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x69, 0x0a, 0x17, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3f, 0x0a, 0x17, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, - 0x63, 0x68, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x69, - 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, - 0x22, 0x4c, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6d, 0x61, 0x70, 0x50, 0x6f, - 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6d, 0x61, 0x70, 0x50, 0x6f, - 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6d, 0x74, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x73, 0x6d, 0x74, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x3a, - 0x0a, 0x1a, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, - 0x6b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x09, 0x6b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x22, 0xac, 0x02, 0x0a, 0x04, 0x55, - 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x1e, 0x0a, 0x0a, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x12, - 0x1a, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64, 0x49, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64, 0x49, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, - 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, - 0x73, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x65, 0x74, - 0x75, 0x70, 0x47, 0x75, 0x69, 0x64, 0x65, 0x53, 0x65, 0x65, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x47, 0x75, 0x69, 0x64, 0x65, 0x53, 0x65, 0x65, - 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x75, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, - 0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, - 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x22, 0x46, 0x0a, 0x14, 0x55, 0x73, 0x65, - 0x72, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, - 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, - 0x65, 0x22, 0x34, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x4d, 0x0a, 0x19, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x75, 0x72, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x3c, 0x0a, 0x12, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, - 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x6c, 0x61, 0x74, - 0x66, 0x6f, 0x72, 0x6d, 0x22, 0xfb, 0x02, 0x0a, 0x0b, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x03, 0x61, 0x70, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0e, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x70, 0x70, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x48, 0x00, 0x52, 0x03, 0x61, 0x70, 0x70, 0x12, 0x28, 0x0a, 0x05, 0x6c, 0x6f, 0x67, 0x69, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x6c, 0x6f, 0x67, - 0x69, 0x6e, 0x12, 0x2b, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, - 0x28, 0x0a, 0x05, 0x63, 0x61, 0x63, 0x68, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x48, 0x00, 0x52, 0x05, 0x63, 0x61, 0x63, 0x68, 0x65, 0x12, 0x3d, 0x0a, 0x0c, 0x6d, 0x61, 0x69, - 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x6d, 0x61, 0x69, 0x6c, - 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x31, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x67, 0x72, 0x70, - 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, - 0x00, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x25, 0x0a, 0x04, 0x6d, - 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x67, 0x72, 0x70, 0x63, - 0x2e, 0x4d, 0x61, 0x69, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x04, 0x6d, 0x61, - 0x69, 0x6c, 0x12, 0x25, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x48, 0x00, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, - 0x6e, 0x74, 0x22, 0x9d, 0x04, 0x0a, 0x08, 0x41, 0x70, 0x70, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, - 0x43, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x49, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x5e, 0x0a, 0x17, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x41, 0x75, - 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x67, - 0x67, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x46, 0x69, 0x6e, 0x69, - 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x17, 0x74, 0x6f, 0x67, - 0x67, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x46, 0x69, 0x6e, 0x69, - 0x73, 0x68, 0x65, 0x64, 0x12, 0x40, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x65, 0x74, 0x46, 0x69, 0x6e, - 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x72, - 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x65, 0x74, 0x46, 0x69, - 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x4c, 0x0a, 0x11, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x42, 0x75, 0x67, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, - 0x75, 0x67, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, - 0x00, 0x52, 0x11, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x46, 0x69, 0x6e, 0x69, - 0x73, 0x68, 0x65, 0x64, 0x12, 0x49, 0x0a, 0x10, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, - 0x67, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x53, - 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x10, 0x72, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, - 0x43, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x45, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x43, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x77, 0x4d, 0x61, 0x69, 0x6e, - 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x68, 0x6f, 0x77, 0x4d, 0x61, 0x69, 0x6e, 0x57, 0x69, 0x6e, 0x64, - 0x6f, 0x77, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x68, 0x6f, 0x77, 0x4d, - 0x61, 0x69, 0x6e, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, - 0x6e, 0x74, 0x22, 0x33, 0x0a, 0x13, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x1e, 0x0a, 0x1c, 0x54, 0x6f, 0x67, 0x67, 0x6c, - 0x65, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, - 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x14, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x65, 0x74, - 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x18, 0x0a, - 0x16, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, - 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x42, 0x75, 0x67, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x68, 0x6f, 0x77, 0x4d, - 0x61, 0x69, 0x6e, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0xe3, - 0x02, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, - 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x42, 0x0a, 0x0c, - 0x74, 0x66, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, - 0x66, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x48, 0x00, 0x52, 0x0c, 0x74, 0x66, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, - 0x12, 0x5b, 0x0a, 0x14, 0x74, 0x77, 0x6f, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x77, 0x6f, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x14, 0x74, 0x77, 0x6f, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x36, 0x0a, - 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x69, - 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x08, 0x66, 0x69, 0x6e, - 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x44, 0x0a, 0x0f, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, - 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x64, 0x49, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, - 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x61, 0x6c, 0x72, 0x65, - 0x61, 0x64, 0x79, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x64, 0x49, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x65, - 0x76, 0x65, 0x6e, 0x74, 0x22, 0x55, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x34, 0x0a, 0x16, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x66, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x22, 0x21, 0x0a, 0x1f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x77, 0x6f, 0x50, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x22, 0x2c, 0x0a, 0x12, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x69, 0x6e, - 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, + 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, + 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, + 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x4c, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x50, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, + 0x69, 0x6d, 0x61, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, + 0x69, 0x6d, 0x61, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6d, 0x74, 0x70, + 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x73, 0x6d, 0x74, 0x70, + 0x50, 0x6f, 0x72, 0x74, 0x22, 0x3a, 0x0a, 0x1a, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, + 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x6b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, + 0x22, 0xac, 0x02, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x54, + 0x65, 0x78, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x76, 0x61, 0x74, 0x61, + 0x72, 0x54, 0x65, 0x78, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64, 0x49, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64, 0x49, + 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x12, + 0x26, 0x0a, 0x0e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x47, 0x75, 0x69, 0x64, 0x65, 0x53, 0x65, 0x65, + 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x47, 0x75, + 0x69, 0x64, 0x65, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x64, 0x42, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x75, 0x73, 0x65, 0x64, + 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0a, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x22, + 0x46, 0x0a, 0x14, 0x55, 0x73, 0x65, 0x72, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, + 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, + 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x22, 0x34, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x05, 0x75, + 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x4d, 0x0a, + 0x19, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x4d, + 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, - 0x49, 0x44, 0x22, 0xb9, 0x04, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, + 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x3c, 0x0a, 0x12, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x6c, 0x61, 0x74, + 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0xfb, 0x02, 0x0a, 0x0b, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x03, 0x61, 0x70, + 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x70, 0x70, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x03, 0x61, 0x70, 0x70, 0x12, 0x28, + 0x0a, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, + 0x00, 0x52, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x2b, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x06, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x63, 0x61, 0x63, 0x68, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x63, 0x61, 0x63, 0x68, 0x65, 0x12, + 0x3d, 0x0a, 0x0c, 0x6d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x69, + 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, + 0x52, 0x0c, 0x6d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x31, + 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x12, 0x25, 0x0a, 0x04, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x69, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x48, 0x00, 0x52, 0x04, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x25, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x42, + 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x9d, 0x04, 0x0a, 0x08, 0x41, 0x70, 0x70, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x43, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x5e, 0x0a, 0x17, 0x74, 0x6f, + 0x67, 0x67, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x46, 0x69, 0x6e, + 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, + 0x00, 0x52, 0x17, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x40, 0x0a, 0x0d, 0x72, 0x65, + 0x73, 0x65, 0x74, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x46, 0x69, + 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x72, + 0x65, 0x73, 0x65, 0x74, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x4c, 0x0a, 0x11, + 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x11, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, + 0x75, 0x67, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x49, 0x0a, 0x10, 0x72, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x42, 0x75, 0x67, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x48, 0x00, 0x52, 0x10, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x53, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x43, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, + 0x75, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x42, 0x75, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x43, 0x0a, 0x0e, 0x73, 0x68, + 0x6f, 0x77, 0x4d, 0x61, 0x69, 0x6e, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x68, 0x6f, 0x77, 0x4d, 0x61, + 0x69, 0x6e, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, + 0x0e, 0x73, 0x68, 0x6f, 0x77, 0x4d, 0x61, 0x69, 0x6e, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x42, + 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x33, 0x0a, 0x13, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x1e, 0x0a, + 0x1c, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x14, 0x0a, + 0x12, 0x52, 0x65, 0x73, 0x65, 0x74, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x22, 0x18, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, + 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x17, 0x0a, + 0x15, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x42, 0x75, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x15, 0x0a, + 0x13, 0x53, 0x68, 0x6f, 0x77, 0x4d, 0x61, 0x69, 0x6e, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x22, 0xe3, 0x02, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x40, 0x0a, 0x0b, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x61, 0x64, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x61, 0x64, 0x79, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, - 0x65, 0x61, 0x64, 0x79, 0x12, 0x58, 0x0a, 0x13, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, - 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, - 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x13, 0x6d, 0x61, 0x6e, 0x75, 0x61, - 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x12, 0x2e, - 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x46, 0x6f, 0x72, 0x63, 0x65, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, 0x53, - 0x0a, 0x13, 0x73, 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, - 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, - 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x48, 0x00, 0x52, 0x13, - 0x73, 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, - 0x64, 0x65, 0x64, 0x12, 0x47, 0x0a, 0x0f, 0x69, 0x73, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, - 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x73, 0x4c, 0x61, 0x74, 0x65, - 0x73, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0f, 0x69, 0x73, 0x4c, - 0x61, 0x74, 0x65, 0x73, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x0d, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x48, 0x00, - 0x52, 0x0d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, - 0x44, 0x0a, 0x0e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x3d, - 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x15, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x32, 0x0a, - 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x61, - 0x64, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x22, 0x20, 0x0a, 0x1e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x75, 0x61, - 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x22, 0x2c, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x46, 0x6f, 0x72, - 0x63, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x22, 0x1b, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x69, 0x6c, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x22, 0x17, - 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x73, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x15, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x22, 0x16, - 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x22, 0xc1, 0x03, 0x0a, 0x0a, 0x43, 0x61, 0x63, 0x68, 0x65, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x63, 0x68, - 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x5f, 0x0a, 0x16, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x63, 0x68, - 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, - 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x16, 0x6c, - 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x53, 0x75, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x61, 0x0a, 0x18, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, - 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x46, - 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x18, - 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, - 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x65, 0x0a, 0x1b, 0x69, 0x73, 0x43, 0x61, - 0x63, 0x68, 0x65, 0x4f, 0x6e, 0x44, 0x69, 0x73, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x73, 0x43, 0x61, 0x63, 0x68, 0x65, 0x4f, 0x6e, 0x44, 0x69, - 0x73, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, - 0x48, 0x00, 0x52, 0x1b, 0x69, 0x73, 0x43, 0x61, 0x63, 0x68, 0x65, 0x4f, 0x6e, 0x44, 0x69, 0x73, - 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, - 0x50, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, - 0x74, 0x68, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x48, 0x00, 0x52, 0x14, 0x64, 0x69, 0x73, - 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x64, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x3b, 0x0a, 0x0f, 0x43, 0x61, - 0x63, 0x68, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x28, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x67, 0x72, - 0x70, 0x63, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, - 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x21, 0x0a, 0x1f, 0x43, 0x61, 0x63, 0x68, 0x65, - 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x75, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x41, 0x0a, 0x1d, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x46, 0x69, - 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x77, - 0x69, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0b, 0x77, 0x69, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x22, 0x37, 0x0a, - 0x1b, 0x49, 0x73, 0x43, 0x61, 0x63, 0x68, 0x65, 0x4f, 0x6e, 0x44, 0x69, 0x73, 0x6b, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x2a, 0x0a, 0x14, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, - 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, - 0x74, 0x68, 0x22, 0x80, 0x02, 0x0a, 0x11, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, - 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x58, - 0x0a, 0x15, 0x75, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, 0x6f, 0x72, 0x53, 0x6d, 0x74, 0x70, 0x46, - 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, 0x6f, 0x72, 0x53, 0x6d, - 0x74, 0x70, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, - 0x00, 0x52, 0x15, 0x75, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, 0x6f, 0x72, 0x53, 0x6d, 0x74, 0x70, - 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x52, 0x0a, 0x13, 0x63, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, - 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x13, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, - 0x6f, 0x72, 0x74, 0x73, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, - 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x49, 0x0a, 0x16, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, - 0x2f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, - 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x1c, 0x0a, 0x1a, 0x55, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, 0x6f, 0x72, 0x53, 0x6d, 0x74, - 0x70, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1a, - 0x0a, 0x18, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x46, 0x69, 0x6e, - 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0xff, 0x01, 0x0a, 0x0d, 0x4b, - 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x5b, 0x0a, 0x16, - 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x69, - 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, - 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, + 0x6f, 0x72, 0x12, 0x42, 0x0a, 0x0c, 0x74, 0x66, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x66, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x74, 0x66, 0x61, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x5b, 0x0a, 0x14, 0x74, 0x77, 0x6f, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x54, 0x77, 0x6f, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x14, 0x74, + 0x77, 0x6f, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x65, 0x64, 0x12, 0x36, 0x0a, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, - 0x00, 0x52, 0x16, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x40, 0x0a, 0x0d, 0x68, 0x61, 0x73, - 0x4e, 0x6f, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x61, 0x73, 0x4e, 0x6f, 0x4b, 0x65, 0x79, - 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x68, 0x61, - 0x73, 0x4e, 0x6f, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x46, 0x0a, 0x0f, 0x72, - 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1d, 0x0a, 0x1b, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x69, - 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x14, 0x0a, 0x12, 0x48, - 0x61, 0x73, 0x4e, 0x6f, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x22, 0x16, 0x0a, 0x14, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0xd9, 0x02, 0x0a, 0x09, 0x4d, 0x61, - 0x69, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x68, 0x0a, 0x1c, 0x6e, 0x6f, 0x41, 0x63, 0x74, + 0x00, 0x52, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x44, 0x0a, 0x0f, 0x61, + 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x64, 0x49, 0x6e, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, + 0x52, 0x0f, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x64, 0x49, + 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x55, 0x0a, 0x0f, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x28, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x34, 0x0a, 0x16, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x66, 0x61, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x21, 0x0a, 0x1f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x54, 0x77, 0x6f, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x2c, 0x0a, 0x12, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x22, 0xb9, 0x04, 0x0a, 0x0b, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, + 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x40, 0x0a, 0x0b, 0x6d, 0x61, 0x6e, 0x75, + 0x61, 0x6c, 0x52, 0x65, 0x61, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x75, 0x61, + 0x6c, 0x52, 0x65, 0x61, 0x64, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x6d, + 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x61, 0x64, 0x79, 0x12, 0x58, 0x0a, 0x13, 0x6d, 0x61, + 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, + 0x13, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, + 0x65, 0x64, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x66, + 0x6f, 0x72, 0x63, 0x65, 0x12, 0x53, 0x0a, 0x13, 0x73, 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, + 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, + 0x65, 0x64, 0x48, 0x00, 0x52, 0x13, 0x73, 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x12, 0x47, 0x0a, 0x0f, 0x69, 0x73, 0x4c, + 0x61, 0x74, 0x65, 0x73, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x49, 0x73, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x48, + 0x00, 0x52, 0x0f, 0x69, 0x73, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x0d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x6e, 0x69, 0x73, + 0x68, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x6e, 0x69, + 0x73, 0x68, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x6e, + 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x44, 0x0a, 0x0e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x65, + 0x76, 0x65, 0x6e, 0x74, 0x22, 0x3d, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x22, 0x32, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, + 0x75, 0x61, 0x6c, 0x52, 0x65, 0x61, 0x64, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x20, 0x0a, 0x1e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, + 0x65, 0x64, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x2c, 0x0a, 0x10, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x1b, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4e, 0x65, + 0x65, 0x64, 0x65, 0x64, 0x22, 0x17, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x73, + 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x15, 0x0a, + 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x6e, 0x69, + 0x73, 0x68, 0x65, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x22, 0xc1, 0x03, 0x0a, + 0x0a, 0x43, 0x61, 0x63, 0x68, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x5f, 0x0a, 0x16, 0x6c, 0x6f, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x53, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x48, 0x00, 0x52, 0x16, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x64, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x61, 0x0a, 0x18, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x46, + 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x43, 0x61, 0x63, 0x68, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x48, 0x00, 0x52, 0x18, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x65, + 0x0a, 0x1b, 0x69, 0x73, 0x43, 0x61, 0x63, 0x68, 0x65, 0x4f, 0x6e, 0x44, 0x69, 0x73, 0x6b, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x73, 0x43, 0x61, 0x63, + 0x68, 0x65, 0x4f, 0x6e, 0x44, 0x69, 0x73, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x48, 0x00, 0x52, 0x1b, 0x69, 0x73, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x4f, 0x6e, 0x44, 0x69, 0x73, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x50, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, + 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x43, + 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x48, + 0x00, 0x52, 0x14, 0x64, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x22, 0x3b, 0x0a, 0x0f, 0x43, 0x61, 0x63, 0x68, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x14, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x21, 0x0a, + 0x1f, 0x43, 0x61, 0x63, 0x68, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x22, 0x41, 0x0a, 0x1d, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, + 0x61, 0x63, 0x68, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x77, 0x69, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x77, 0x69, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x1b, 0x49, 0x73, 0x43, 0x61, 0x63, 0x68, 0x65, 0x4f, 0x6e, + 0x44, 0x69, 0x73, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x2a, 0x0a, 0x14, + 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x80, 0x02, 0x0a, 0x11, 0x4d, 0x61, 0x69, + 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x34, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x58, 0x0a, 0x15, 0x75, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, 0x6f, + 0x72, 0x53, 0x6d, 0x74, 0x70, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x53, 0x73, + 0x6c, 0x46, 0x6f, 0x72, 0x53, 0x6d, 0x74, 0x70, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x15, 0x75, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, + 0x6f, 0x72, 0x53, 0x6d, 0x74, 0x70, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x52, + 0x0a, 0x13, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x46, 0x69, 0x6e, + 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x46, 0x69, + 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x13, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, + 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x49, 0x0a, 0x16, 0x4d, + 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x69, 0x6c, 0x53, + 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x1c, 0x0a, 0x1a, 0x55, 0x73, 0x65, 0x53, 0x73, 0x6c, + 0x46, 0x6f, 0x72, 0x53, 0x6d, 0x74, 0x70, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1a, 0x0a, 0x18, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, + 0x72, 0x74, 0x73, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x22, 0xff, 0x01, 0x0a, 0x0d, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x12, 0x5b, 0x0a, 0x16, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x16, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4b, + 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, + 0x40, 0x0a, 0x0d, 0x68, 0x61, 0x73, 0x4e, 0x6f, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x61, + 0x73, 0x4e, 0x6f, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x48, 0x00, 0x52, 0x0d, 0x68, 0x61, 0x73, 0x4e, 0x6f, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x12, 0x46, 0x0a, 0x0f, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x22, 0x1d, 0x0a, 0x1b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x22, 0x14, 0x0a, 0x12, 0x48, 0x61, 0x73, 0x4e, 0x6f, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x16, 0x0a, 0x14, 0x52, 0x65, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, + 0xd9, 0x02, 0x0a, 0x09, 0x4d, 0x61, 0x69, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x68, 0x0a, + 0x1c, 0x6e, 0x6f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x52, + 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, - 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4b, 0x65, 0x79, - 0x46, 0x6f, 0x72, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x48, 0x00, 0x52, 0x1c, 0x6e, 0x6f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4b, 0x65, 0x79, - 0x46, 0x6f, 0x72, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x12, 0x43, 0x0a, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x72, 0x70, 0x63, - 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x55, 0x0a, 0x14, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x14, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x3d, 0x0a, - 0x0c, 0x61, 0x70, 0x69, 0x43, 0x65, 0x72, 0x74, 0x49, 0x73, 0x73, 0x75, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x70, 0x69, 0x43, 0x65, - 0x72, 0x74, 0x49, 0x73, 0x73, 0x75, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0c, - 0x61, 0x70, 0x69, 0x43, 0x65, 0x72, 0x74, 0x49, 0x73, 0x73, 0x75, 0x65, 0x42, 0x07, 0x0a, 0x05, - 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x34, 0x0a, 0x1c, 0x4e, 0x6f, 0x41, 0x63, 0x74, 0x69, 0x76, - 0x65, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x2f, 0x0a, 0x13, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x35, 0x0a, 0x19, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x4c, 0x6f, - 0x67, 0x6f, 0x75, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x22, 0x13, 0x0a, 0x11, 0x41, 0x70, 0x69, 0x43, 0x65, 0x72, 0x74, 0x49, 0x73, - 0x73, 0x75, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0xfb, 0x01, 0x0a, 0x09, 0x55, 0x73, 0x65, - 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x5e, 0x0a, 0x17, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, - 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x54, - 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x46, 0x69, - 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x17, 0x74, - 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x46, 0x69, - 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x49, 0x0a, 0x10, 0x75, 0x73, 0x65, 0x72, 0x44, 0x69, - 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, - 0x10, 0x75, 0x73, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, - 0x64, 0x12, 0x3a, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, - 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, - 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x42, 0x07, 0x0a, - 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x36, 0x0a, 0x1c, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, - 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, - 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x22, 0x33, - 0x0a, 0x15, 0x55, 0x73, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, - 0x61, 0x6d, 0x65, 0x22, 0x2a, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x2a, - 0x71, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0d, 0x0a, 0x09, 0x4c, - 0x4f, 0x47, 0x5f, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, - 0x47, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x47, - 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x47, 0x5f, - 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x47, 0x5f, 0x49, 0x4e, - 0x46, 0x4f, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x47, 0x5f, 0x44, 0x45, 0x42, 0x55, - 0x47, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x47, 0x5f, 0x54, 0x52, 0x41, 0x43, 0x45, - 0x10, 0x06, 0x2a, 0xa2, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x45, 0x72, 0x72, 0x6f, - 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x55, 0x53, 0x45, 0x52, 0x4e, 0x41, 0x4d, - 0x45, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x52, 0x45, 0x45, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x10, - 0x01, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x46, 0x41, 0x5f, 0x45, - 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x46, 0x41, 0x5f, 0x41, 0x42, - 0x4f, 0x52, 0x54, 0x10, 0x04, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x57, 0x4f, 0x5f, 0x50, 0x41, 0x53, - 0x53, 0x57, 0x4f, 0x52, 0x44, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x12, 0x17, - 0x0a, 0x13, 0x54, 0x57, 0x4f, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x53, 0x5f, - 0x41, 0x42, 0x4f, 0x52, 0x54, 0x10, 0x06, 0x2a, 0x5b, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x50, - 0x44, 0x41, 0x54, 0x45, 0x5f, 0x4d, 0x41, 0x4e, 0x55, 0x41, 0x4c, 0x5f, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x4f, - 0x52, 0x43, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x55, - 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x49, 0x4c, 0x45, 0x4e, 0x54, 0x5f, 0x45, 0x52, 0x52, - 0x4f, 0x52, 0x10, 0x02, 0x2a, 0x57, 0x0a, 0x0e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x41, 0x43, 0x48, 0x45, 0x5f, - 0x55, 0x4e, 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x41, 0x43, 0x48, 0x45, 0x5f, 0x43, 0x41, 0x4e, - 0x54, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x0d, - 0x0a, 0x09, 0x44, 0x49, 0x53, 0x4b, 0x5f, 0x46, 0x55, 0x4c, 0x4c, 0x10, 0x02, 0x2a, 0x41, 0x0a, - 0x15, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4d, 0x41, 0x50, 0x5f, 0x50, - 0x4f, 0x52, 0x54, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x53, - 0x4d, 0x54, 0x50, 0x5f, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x01, - 0x32, 0xe7, 0x1f, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x4c, 0x6f, 0x67, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, - 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3a, 0x0a, 0x08, 0x47, 0x75, 0x69, 0x52, 0x65, - 0x61, 0x64, 0x79, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x04, 0x51, 0x75, 0x69, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x07, 0x52, - 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, + 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x1c, 0x6e, 0x6f, 0x41, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, + 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x43, 0x0a, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x55, 0x0a, 0x14, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x4c, 0x6f, + 0x67, 0x6f, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, + 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x14, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x4c, 0x6f, 0x67, + 0x6f, 0x75, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x61, 0x70, 0x69, 0x43, 0x65, 0x72, 0x74, 0x49, 0x73, + 0x73, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x41, 0x70, 0x69, 0x43, 0x65, 0x72, 0x74, 0x49, 0x73, 0x73, 0x75, 0x65, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x61, 0x70, 0x69, 0x43, 0x65, 0x72, 0x74, 0x49, 0x73, 0x73, + 0x75, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x34, 0x0a, 0x1c, 0x4e, + 0x6f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x63, + 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x22, 0x2f, 0x0a, 0x13, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x22, 0x35, 0x0a, 0x19, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x64, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, + 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x13, 0x0a, 0x11, 0x41, 0x70, 0x69, + 0x43, 0x65, 0x72, 0x74, 0x49, 0x73, 0x73, 0x75, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0xfb, + 0x01, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x5e, 0x0a, 0x17, + 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x46, + 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x53, 0x70, 0x6c, 0x69, 0x74, + 0x4d, 0x6f, 0x64, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x48, 0x00, 0x52, 0x17, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x53, 0x70, 0x6c, 0x69, 0x74, + 0x4d, 0x6f, 0x64, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x49, 0x0a, 0x10, + 0x75, 0x73, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x10, 0x75, 0x73, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x3a, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x36, 0x0a, 0x1c, + 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x46, + 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, + 0x65, 0x72, 0x49, 0x44, 0x22, 0x33, 0x0a, 0x15, 0x55, 0x73, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2a, 0x0a, 0x10, 0x55, 0x73, 0x65, + 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x44, 0x2a, 0x71, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x47, 0x5f, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x00, + 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x47, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x01, 0x12, + 0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, + 0x0a, 0x08, 0x4c, 0x4f, 0x47, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, + 0x4c, 0x4f, 0x47, 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, + 0x47, 0x5f, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x47, + 0x5f, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x06, 0x2a, 0xa2, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x55, + 0x53, 0x45, 0x52, 0x4e, 0x41, 0x4d, 0x45, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x52, 0x45, 0x45, + 0x5f, 0x55, 0x53, 0x45, 0x52, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4f, 0x4e, 0x4e, 0x45, + 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, + 0x09, 0x54, 0x46, 0x41, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, + 0x54, 0x46, 0x41, 0x5f, 0x41, 0x42, 0x4f, 0x52, 0x54, 0x10, 0x04, 0x12, 0x17, 0x0a, 0x13, 0x54, + 0x57, 0x4f, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x53, 0x5f, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x05, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x57, 0x4f, 0x5f, 0x50, 0x41, 0x53, 0x53, + 0x57, 0x4f, 0x52, 0x44, 0x53, 0x5f, 0x41, 0x42, 0x4f, 0x52, 0x54, 0x10, 0x06, 0x2a, 0x5b, 0x0a, + 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x17, 0x0a, 0x13, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x4d, 0x41, 0x4e, 0x55, 0x41, + 0x4c, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x50, 0x44, + 0x41, 0x54, 0x45, 0x5f, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, + 0x01, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x49, 0x4c, 0x45, + 0x4e, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x2a, 0x57, 0x0a, 0x0e, 0x43, 0x61, + 0x63, 0x68, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, + 0x43, 0x41, 0x43, 0x48, 0x45, 0x5f, 0x55, 0x4e, 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, 0x4c, + 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x41, 0x43, + 0x48, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x54, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x49, 0x53, 0x4b, 0x5f, 0x46, 0x55, 0x4c, + 0x4c, 0x10, 0x02, 0x2a, 0x41, 0x0a, 0x15, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, + 0x49, 0x4d, 0x41, 0x50, 0x5f, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, + 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x4d, 0x54, 0x50, 0x5f, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x49, + 0x53, 0x53, 0x55, 0x45, 0x10, 0x01, 0x32, 0x9b, 0x1f, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, + 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x0d, 0x53, 0x68, 0x6f, 0x77, 0x4f, 0x6e, - 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x53, - 0x68, 0x6f, 0x77, 0x53, 0x70, 0x6c, 0x61, 0x73, 0x68, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x12, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x45, 0x0a, 0x0f, 0x49, 0x73, 0x46, 0x69, 0x72, 0x73, 0x74, 0x47, 0x75, - 0x69, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x53, 0x65, - 0x74, 0x49, 0x73, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4f, 0x6e, 0x12, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x43, 0x0a, 0x0d, 0x49, 0x73, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x4f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, - 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x49, 0x73, - 0x42, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, - 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x43, 0x0a, 0x0d, 0x49, 0x73, 0x42, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x49, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x49, 0x73, 0x41, 0x6c, 0x6c, - 0x4d, 0x61, 0x69, 0x6c, 0x56, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, - 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x46, 0x0a, 0x10, 0x49, 0x73, 0x41, 0x6c, 0x6c, 0x4d, 0x61, 0x69, 0x6c, 0x56, 0x69, 0x73, 0x69, - 0x62, 0x6c, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, - 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3c, 0x0a, 0x04, 0x47, 0x6f, 0x4f, 0x73, 0x12, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, - 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x40, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x73, 0x50, 0x61, - 0x74, 0x68, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x43, 0x0a, 0x0b, 0x4c, 0x69, 0x63, 0x65, - 0x6e, 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4c, 0x0a, - 0x14, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x73, 0x50, 0x61, 0x67, - 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4e, 0x0a, 0x16, 0x44, - 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, - 0x73, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x47, 0x0a, 0x0f, 0x4c, - 0x61, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x67, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, - 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x47, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x43, 0x75, 0x72, - 0x72, 0x65, 0x6e, 0x74, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3b, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, - 0x75, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x42, 0x75, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x45, 0x0a, 0x0d, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x4c, 0x61, 0x75, 0x6e, 0x63, - 0x68, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x11, 0x53, 0x65, 0x74, - 0x4d, 0x61, 0x69, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x08, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x32, 0x46, 0x41, 0x12, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3f, 0x0a, 0x0b, + 0x41, 0x64, 0x64, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3a, 0x0a, + 0x08, 0x47, 0x75, 0x69, 0x52, 0x65, 0x61, 0x64, 0x79, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x3d, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x32, 0x50, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x73, 0x12, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x3d, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x17, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x3d, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, - 0x0a, 0x0d, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x4c, 0x0a, 0x16, 0x53, 0x65, 0x74, 0x49, 0x73, 0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, - 0x63, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x6e, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, - 0x13, 0x49, 0x73, 0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x4f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, - 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4a, 0x0a, 0x14, 0x49, 0x73, 0x43, 0x61, - 0x63, 0x68, 0x65, 0x4f, 0x6e, 0x44, 0x69, 0x73, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x45, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, - 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x49, 0x0a, 0x10, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, - 0x1d, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, - 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x45, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x49, 0x73, 0x44, - 0x6f, 0x48, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, - 0x0c, 0x49, 0x73, 0x44, 0x6f, 0x48, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x2e, + 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x04, 0x51, 0x75, 0x69, + 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x12, 0x39, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x0d, + 0x53, 0x68, 0x6f, 0x77, 0x4f, 0x6e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x46, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, 0x6f, - 0x72, 0x53, 0x6d, 0x74, 0x70, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x0d, 0x55, 0x73, 0x65, - 0x53, 0x73, 0x6c, 0x46, 0x6f, 0x72, 0x53, 0x6d, 0x74, 0x70, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x40, - 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x3f, 0x0a, 0x08, 0x49, 0x6d, 0x61, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x3f, 0x0a, 0x08, 0x53, 0x6d, 0x74, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x16, 0x2e, + 0x65, 0x12, 0x46, 0x0a, 0x10, 0x53, 0x68, 0x6f, 0x77, 0x53, 0x70, 0x6c, 0x61, 0x73, 0x68, 0x53, + 0x63, 0x72, 0x65, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, - 0x73, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, - 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x45, 0x0a, 0x0a, 0x49, 0x73, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x72, 0x65, - 0x65, 0x12, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1a, + 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x45, 0x0a, 0x0f, 0x49, 0x73, 0x46, + 0x69, 0x72, 0x73, 0x74, 0x47, 0x75, 0x69, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x46, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x49, 0x73, 0x41, 0x75, 0x74, 0x6f, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x4f, 0x6e, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x0d, 0x49, 0x73, 0x41, 0x75, + 0x74, 0x6f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x46, 0x0a, + 0x10, 0x53, 0x65, 0x74, 0x49, 0x73, 0x42, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x0d, 0x49, 0x73, 0x42, 0x65, 0x74, 0x61, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4e, 0x0a, 0x12, 0x41, 0x76, - 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, - 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65, - 0x74, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x49, 0x0a, 0x13, 0x53, 0x65, + 0x74, 0x49, 0x73, 0x41, 0x6c, 0x6c, 0x4d, 0x61, 0x69, 0x6c, 0x56, 0x69, 0x73, 0x69, 0x62, 0x6c, + 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x49, 0x73, 0x41, 0x6c, 0x6c, 0x4d, 0x61, + 0x69, 0x6c, 0x56, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3c, 0x0a, + 0x04, 0x47, 0x6f, 0x4f, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x54, + 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x07, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x40, 0x0a, 0x08, + 0x4c, 0x6f, 0x67, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x43, + 0x0a, 0x0b, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x4c, 0x0a, 0x14, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, + 0x74, 0x65, 0x73, 0x50, 0x61, 0x67, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x4e, 0x0a, 0x16, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, + 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x47, 0x0a, 0x0f, 0x4c, 0x61, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x67, 0x65, + 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65, + 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x47, 0x0a, 0x0f, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, - 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x47, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, + 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x3d, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, - 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, - 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x0a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, - 0x73, 0x65, 0x72, 0x12, 0x46, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x70, - 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, - 0x73, 0x65, 0x72, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x4c, - 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x4a, 0x0a, 0x12, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3b, 0x0a, 0x09, 0x52, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x45, 0x0a, 0x0d, 0x46, 0x6f, 0x72, 0x63, + 0x65, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x42, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x51, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, - 0x55, 0x73, 0x65, 0x72, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x12, 0x1f, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x41, 0x70, - 0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x49, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x4d, 0x61, 0x69, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x12, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x36, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x32, 0x46, 0x41, 0x12, 0x12, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x32, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x12, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, + 0x62, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0d, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x16, 0x53, 0x65, 0x74, 0x49, 0x73, 0x41, 0x75, + 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x6e, 0x12, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x13, 0x49, 0x73, 0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, + 0x69, 0x63, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, - 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, - 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, - 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x45, + 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x49, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, + 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1d, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x45, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x49, 0x73, 0x44, 0x6f, 0x48, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0c, 0x49, 0x73, 0x44, 0x6f, 0x48, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x53, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, 0x6f, 0x72, 0x53, 0x6d, 0x74, 0x70, 0x12, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x0d, 0x55, 0x73, 0x65, 0x53, 0x73, 0x6c, 0x46, 0x6f, 0x72, + 0x53, 0x6d, 0x74, 0x70, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, + 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x40, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3f, 0x0a, 0x08, 0x49, 0x6d, + 0x61, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3f, 0x0a, 0x08, 0x53, + 0x6d, 0x74, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3f, 0x0a, 0x0b, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x45, 0x0a, + 0x0a, 0x49, 0x73, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x72, 0x65, 0x65, 0x12, 0x1b, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, + 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4e, 0x0a, 0x12, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, + 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, + 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x43, 0x75, 0x72, 0x72, 0x65, + 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x47, 0x0a, 0x0f, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3d, 0x0a, 0x0b, 0x47, 0x65, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x1a, 0x0a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x46, 0x0a, + 0x10, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, + 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x53, 0x70, 0x6c, + 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x51, 0x0a, + 0x16, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x55, 0x73, 0x65, 0x72, 0x41, 0x70, + 0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x12, 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, + 0x01, 0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -4627,90 +4612,88 @@ var file_bridge_proto_depIdxs = []int32{ 64, // 85: grpc.Bridge.InstallUpdate:input_type -> google.protobuf.Empty 65, // 86: grpc.Bridge.SetIsAutomaticUpdateOn:input_type -> google.protobuf.BoolValue 64, // 87: grpc.Bridge.IsAutomaticUpdateOn:input_type -> google.protobuf.Empty - 64, // 88: grpc.Bridge.IsCacheOnDiskEnabled:input_type -> google.protobuf.Empty - 64, // 89: grpc.Bridge.DiskCachePath:input_type -> google.protobuf.Empty - 9, // 90: grpc.Bridge.ChangeLocalCache:input_type -> grpc.ChangeLocalCacheRequest - 65, // 91: grpc.Bridge.SetIsDoHEnabled:input_type -> google.protobuf.BoolValue - 64, // 92: grpc.Bridge.IsDoHEnabled:input_type -> google.protobuf.Empty - 65, // 93: grpc.Bridge.SetUseSslForSmtp:input_type -> google.protobuf.BoolValue - 64, // 94: grpc.Bridge.UseSslForSmtp:input_type -> google.protobuf.Empty - 64, // 95: grpc.Bridge.Hostname:input_type -> google.protobuf.Empty - 64, // 96: grpc.Bridge.ImapPort:input_type -> google.protobuf.Empty - 64, // 97: grpc.Bridge.SmtpPort:input_type -> google.protobuf.Empty - 10, // 98: grpc.Bridge.ChangePorts:input_type -> grpc.ChangePortsRequest - 66, // 99: grpc.Bridge.IsPortFree:input_type -> google.protobuf.Int32Value - 64, // 100: grpc.Bridge.AvailableKeychains:input_type -> google.protobuf.Empty - 63, // 101: grpc.Bridge.SetCurrentKeychain:input_type -> google.protobuf.StringValue - 64, // 102: grpc.Bridge.CurrentKeychain:input_type -> google.protobuf.Empty - 64, // 103: grpc.Bridge.GetUserList:input_type -> google.protobuf.Empty - 63, // 104: grpc.Bridge.GetUser:input_type -> google.protobuf.StringValue - 13, // 105: grpc.Bridge.SetUserSplitMode:input_type -> grpc.UserSplitModeRequest - 63, // 106: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue - 63, // 107: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue - 15, // 108: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest - 16, // 109: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest - 64, // 110: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty - 63, // 111: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue - 64, // 112: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty - 64, // 113: grpc.Bridge.GuiReady:output_type -> google.protobuf.Empty - 64, // 114: grpc.Bridge.Quit:output_type -> google.protobuf.Empty - 64, // 115: grpc.Bridge.Restart:output_type -> google.protobuf.Empty - 65, // 116: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue - 65, // 117: grpc.Bridge.ShowSplashScreen:output_type -> google.protobuf.BoolValue - 65, // 118: grpc.Bridge.IsFirstGuiStart:output_type -> google.protobuf.BoolValue - 64, // 119: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty - 65, // 120: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue - 64, // 121: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty - 65, // 122: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue - 64, // 123: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty - 65, // 124: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue - 63, // 125: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue - 64, // 126: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty - 63, // 127: grpc.Bridge.Version:output_type -> google.protobuf.StringValue - 63, // 128: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue - 63, // 129: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue - 63, // 130: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue - 63, // 131: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue - 63, // 132: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue - 64, // 133: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty - 63, // 134: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue - 63, // 135: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue - 64, // 136: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty - 64, // 137: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty - 64, // 138: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty - 64, // 139: grpc.Bridge.Login:output_type -> google.protobuf.Empty - 64, // 140: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty - 64, // 141: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty - 64, // 142: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty - 64, // 143: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty - 64, // 144: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty - 64, // 145: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty - 65, // 146: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue - 65, // 147: grpc.Bridge.IsCacheOnDiskEnabled:output_type -> google.protobuf.BoolValue - 63, // 148: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue - 64, // 149: grpc.Bridge.ChangeLocalCache:output_type -> google.protobuf.Empty - 64, // 150: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty - 65, // 151: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue - 64, // 152: grpc.Bridge.SetUseSslForSmtp:output_type -> google.protobuf.Empty - 65, // 153: grpc.Bridge.UseSslForSmtp:output_type -> google.protobuf.BoolValue - 63, // 154: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue - 66, // 155: grpc.Bridge.ImapPort:output_type -> google.protobuf.Int32Value - 66, // 156: grpc.Bridge.SmtpPort:output_type -> google.protobuf.Int32Value - 64, // 157: grpc.Bridge.ChangePorts:output_type -> google.protobuf.Empty - 65, // 158: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue - 11, // 159: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse - 64, // 160: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty - 63, // 161: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue - 14, // 162: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse - 12, // 163: grpc.Bridge.GetUser:output_type -> grpc.User - 64, // 164: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty - 64, // 165: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty - 64, // 166: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty - 64, // 167: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty - 17, // 168: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent - 64, // 169: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty - 111, // [111:170] is the sub-list for method output_type - 52, // [52:111] is the sub-list for method input_type + 64, // 88: grpc.Bridge.DiskCachePath:input_type -> google.protobuf.Empty + 9, // 89: grpc.Bridge.ChangeLocalCache:input_type -> grpc.ChangeLocalCacheRequest + 65, // 90: grpc.Bridge.SetIsDoHEnabled:input_type -> google.protobuf.BoolValue + 64, // 91: grpc.Bridge.IsDoHEnabled:input_type -> google.protobuf.Empty + 65, // 92: grpc.Bridge.SetUseSslForSmtp:input_type -> google.protobuf.BoolValue + 64, // 93: grpc.Bridge.UseSslForSmtp:input_type -> google.protobuf.Empty + 64, // 94: grpc.Bridge.Hostname:input_type -> google.protobuf.Empty + 64, // 95: grpc.Bridge.ImapPort:input_type -> google.protobuf.Empty + 64, // 96: grpc.Bridge.SmtpPort:input_type -> google.protobuf.Empty + 10, // 97: grpc.Bridge.ChangePorts:input_type -> grpc.ChangePortsRequest + 66, // 98: grpc.Bridge.IsPortFree:input_type -> google.protobuf.Int32Value + 64, // 99: grpc.Bridge.AvailableKeychains:input_type -> google.protobuf.Empty + 63, // 100: grpc.Bridge.SetCurrentKeychain:input_type -> google.protobuf.StringValue + 64, // 101: grpc.Bridge.CurrentKeychain:input_type -> google.protobuf.Empty + 64, // 102: grpc.Bridge.GetUserList:input_type -> google.protobuf.Empty + 63, // 103: grpc.Bridge.GetUser:input_type -> google.protobuf.StringValue + 13, // 104: grpc.Bridge.SetUserSplitMode:input_type -> grpc.UserSplitModeRequest + 63, // 105: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue + 63, // 106: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue + 15, // 107: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest + 16, // 108: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest + 64, // 109: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty + 63, // 110: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue + 64, // 111: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty + 64, // 112: grpc.Bridge.GuiReady:output_type -> google.protobuf.Empty + 64, // 113: grpc.Bridge.Quit:output_type -> google.protobuf.Empty + 64, // 114: grpc.Bridge.Restart:output_type -> google.protobuf.Empty + 65, // 115: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue + 65, // 116: grpc.Bridge.ShowSplashScreen:output_type -> google.protobuf.BoolValue + 65, // 117: grpc.Bridge.IsFirstGuiStart:output_type -> google.protobuf.BoolValue + 64, // 118: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty + 65, // 119: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue + 64, // 120: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty + 65, // 121: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue + 64, // 122: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty + 65, // 123: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue + 63, // 124: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue + 64, // 125: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty + 63, // 126: grpc.Bridge.Version:output_type -> google.protobuf.StringValue + 63, // 127: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue + 63, // 128: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue + 63, // 129: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue + 63, // 130: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue + 63, // 131: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue + 64, // 132: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty + 63, // 133: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue + 63, // 134: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue + 64, // 135: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty + 64, // 136: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty + 64, // 137: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty + 64, // 138: grpc.Bridge.Login:output_type -> google.protobuf.Empty + 64, // 139: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty + 64, // 140: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty + 64, // 141: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty + 64, // 142: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty + 64, // 143: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty + 64, // 144: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty + 65, // 145: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue + 63, // 146: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue + 64, // 147: grpc.Bridge.ChangeLocalCache:output_type -> google.protobuf.Empty + 64, // 148: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty + 65, // 149: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue + 64, // 150: grpc.Bridge.SetUseSslForSmtp:output_type -> google.protobuf.Empty + 65, // 151: grpc.Bridge.UseSslForSmtp:output_type -> google.protobuf.BoolValue + 63, // 152: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue + 66, // 153: grpc.Bridge.ImapPort:output_type -> google.protobuf.Int32Value + 66, // 154: grpc.Bridge.SmtpPort:output_type -> google.protobuf.Int32Value + 64, // 155: grpc.Bridge.ChangePorts:output_type -> google.protobuf.Empty + 65, // 156: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue + 11, // 157: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse + 64, // 158: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty + 63, // 159: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue + 14, // 160: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse + 12, // 161: grpc.Bridge.GetUser:output_type -> grpc.User + 64, // 162: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty + 64, // 163: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty + 64, // 164: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty + 64, // 165: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty + 17, // 166: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent + 64, // 167: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty + 110, // [110:168] is the sub-list for method output_type + 52, // [52:110] is the sub-list for method input_type 52, // [52:52] is the sub-list for extension type_name 52, // [52:52] is the sub-list for extension extendee 0, // [0:52] is the sub-list for field type_name diff --git a/internal/frontend/grpc/bridge.proto b/internal/frontend/grpc/bridge.proto index 467f4843..43490265 100644 --- a/internal/frontend/grpc/bridge.proto +++ b/internal/frontend/grpc/bridge.proto @@ -72,7 +72,6 @@ service Bridge { rpc IsAutomaticUpdateOn(google.protobuf.Empty) returns (google.protobuf.BoolValue); // cache - rpc IsCacheOnDiskEnabled (google.protobuf.Empty) returns (google.protobuf.BoolValue); rpc DiskCachePath(google.protobuf.Empty) returns (google.protobuf.StringValue); rpc ChangeLocalCache(ChangeLocalCacheRequest) returns (google.protobuf.Empty); @@ -160,7 +159,6 @@ message LoginAbortRequest { // Cache on disk related message //********************************************************** message ChangeLocalCacheRequest { - bool enableDiskCache = 1; string diskCachePath = 2; } diff --git a/internal/frontend/grpc/bridge_grpc.pb.go b/internal/frontend/grpc/bridge_grpc.pb.go index 6e18e003..6e60a11f 100644 --- a/internal/frontend/grpc/bridge_grpc.pb.go +++ b/internal/frontend/grpc/bridge_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v3.21.3 +// - protoc v3.21.7 // source: bridge.proto package grpc @@ -64,7 +64,6 @@ type BridgeClient interface { SetIsAutomaticUpdateOn(ctx context.Context, in *wrapperspb.BoolValue, opts ...grpc.CallOption) (*emptypb.Empty, error) IsAutomaticUpdateOn(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) // cache - IsCacheOnDiskEnabled(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) DiskCachePath(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.StringValue, error) ChangeLocalCache(ctx context.Context, in *ChangeLocalCacheRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // mail @@ -425,15 +424,6 @@ func (c *bridgeClient) IsAutomaticUpdateOn(ctx context.Context, in *emptypb.Empt return out, nil } -func (c *bridgeClient) IsCacheOnDiskEnabled(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) { - out := new(wrapperspb.BoolValue) - err := c.cc.Invoke(ctx, "/grpc.Bridge/IsCacheOnDiskEnabled", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *bridgeClient) DiskCachePath(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.StringValue, error) { out := new(wrapperspb.StringValue) err := c.cc.Invoke(ctx, "/grpc.Bridge/DiskCachePath", in, out, opts...) @@ -699,7 +689,6 @@ type BridgeServer interface { SetIsAutomaticUpdateOn(context.Context, *wrapperspb.BoolValue) (*emptypb.Empty, error) IsAutomaticUpdateOn(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) // cache - IsCacheOnDiskEnabled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) DiskCachePath(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) ChangeLocalCache(context.Context, *ChangeLocalCacheRequest) (*emptypb.Empty, error) // mail @@ -841,9 +830,6 @@ func (UnimplementedBridgeServer) SetIsAutomaticUpdateOn(context.Context, *wrappe func (UnimplementedBridgeServer) IsAutomaticUpdateOn(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) { return nil, status.Errorf(codes.Unimplemented, "method IsAutomaticUpdateOn not implemented") } -func (UnimplementedBridgeServer) IsCacheOnDiskEnabled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) { - return nil, status.Errorf(codes.Unimplemented, "method IsCacheOnDiskEnabled not implemented") -} func (UnimplementedBridgeServer) DiskCachePath(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) { return nil, status.Errorf(codes.Unimplemented, "method DiskCachePath not implemented") } @@ -1571,24 +1557,6 @@ func _Bridge_IsAutomaticUpdateOn_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } -func _Bridge_IsCacheOnDiskEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(BridgeServer).IsCacheOnDiskEnabled(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/grpc.Bridge/IsCacheOnDiskEnabled", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(BridgeServer).IsCacheOnDiskEnabled(ctx, req.(*emptypb.Empty)) - } - return interceptor(ctx, in, info, handler) -} - func _Bridge_DiskCachePath_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -2139,10 +2107,6 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{ MethodName: "IsAutomaticUpdateOn", Handler: _Bridge_IsAutomaticUpdateOn_Handler, }, - { - MethodName: "IsCacheOnDiskEnabled", - Handler: _Bridge_IsCacheOnDiskEnabled_Handler, - }, { MethodName: "DiskCachePath", Handler: _Bridge_DiskCachePath_Handler, diff --git a/internal/serverutil/error_logger.go b/internal/frontend/grpc/event_utils.go similarity index 57% rename from internal/serverutil/error_logger.go rename to internal/frontend/grpc/event_utils.go index 6bc14267..cbbab163 100644 --- a/internal/serverutil/error_logger.go +++ b/internal/frontend/grpc/event_utils.go @@ -15,25 +15,18 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package serverutil +package grpc -import ( - "github.com/sirupsen/logrus" -) +import "github.com/bradenaw/juniper/xslices" -// ServerErrorLogger implements go-imap/logger interface. -type ServerErrorLogger struct { - l *logrus.Entry +// isInternetStatus returns true iff the event is InternetStatus. +func (x *StreamEvent) isInternetStatus() bool { + appEvent := x.GetApp() + + return (appEvent != nil) && (appEvent.GetInternetStatus() != nil) } -func NewServerErrorLogger(protocol Protocol) *ServerErrorLogger { - return &ServerErrorLogger{l: logrus.WithField("protocol", protocol)} -} - -func (s *ServerErrorLogger) Printf(format string, args ...interface{}) { - s.l.Errorf(format, args...) -} - -func (s *ServerErrorLogger) Println(args ...interface{}) { - s.l.Errorln(args...) +// filterOutInternetStatusEvents return a copy of the events list where all internet connection events have been removed. +func filterOutInternetStatusEvents(events []*StreamEvent) []*StreamEvent { + return xslices.Filter(events, func(event *StreamEvent) bool { return !event.isInternetStatus() }) } diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index a03c773c..e82c09c8 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -21,34 +21,29 @@ package grpc import ( "context" - cryptotls "crypto/tls" + "crypto/tls" + "errors" "fmt" "net" "path/filepath" - "strings" "sync" - "time" "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/config/tls" + "github.com/ProtonMail/proton-bridge/v2/internal/certs" + "github.com/ProtonMail/proton-bridge/v2/internal/crash" "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/frontend/types" "github.com/ProtonMail/proton-bridge/v2/internal/locations" "github.com/ProtonMail/proton-bridge/v2/internal/updater" - "github.com/ProtonMail/proton-bridge/v2/internal/users" - "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" + "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/ProtonMail/proton-bridge/v2/pkg/restarter" "github.com/google/uuid" - "github.com/pkg/errors" "github.com/sirupsen/logrus" + "gitlab.protontech.ch/go/liteapi" "google.golang.org/grpc" - "google.golang.org/grpc/codes" + codes "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" + status "google.golang.org/grpc/status" ) const ( @@ -59,6 +54,7 @@ const ( // Service is the RPC service struct. type Service struct { // nolint:structcheck UnimplementedBridgeServer + grpcServer *grpc.Server // the gGRPC server listener net.Listener eventStreamCh chan *StreamEvent @@ -66,99 +62,87 @@ type Service struct { // nolint:structcheck eventQueue []*StreamEvent eventQueueMutex sync.Mutex - panicHandler types.PanicHandler - eventListener listener.Listener - updater types.Updater - updateCheckMutex sync.Mutex - bridge types.Bridger - restarter types.Restarter - showOnStartup bool - authClient pmapi.Client - auth *pmapi.Auth - password []byte - newVersionInfo updater.VersionInfo + panicHandler *crash.Handler + restarter *restarter.Restarter + bridge *bridge.Bridge + newVersionInfo updater.VersionInfo + log *logrus.Entry initializing sync.WaitGroup initializationDone sync.Once firstTimeAutostart sync.Once - locations *locations.Locations - token string - pemCert string + + showOnStartup bool } // NewService returns a new instance of the service. func NewService( - showOnStartup bool, - panicHandler types.PanicHandler, - eventListener listener.Listener, - updater types.Updater, - bridge types.Bridger, - restarter types.Restarter, + panicHandler *crash.Handler, + restarter *restarter.Restarter, locations *locations.Locations, -) *Service { - s := Service{ - UnimplementedBridgeServer: UnimplementedBridgeServer{}, - panicHandler: panicHandler, - eventListener: eventListener, - updater: updater, - bridge: bridge, - restarter: restarter, - showOnStartup: showOnStartup, + bridge *bridge.Bridge, + showOnStartup bool, +) (*Service, error) { + tlsConfig, certPEM, err := newTLSConfig() + if err != nil { + logrus.WithError(err).Panic("Could not generate gRPC TLS config") + } + + listener, err := net.Listen("tcp", "127.0.0.1:0") // Port should be provided by the OS. + if err != nil { + logrus.WithError(err).Panic("Could not create gRPC listener") + } + + token := uuid.NewString() + + if path, err := saveGRPCServerConfigFile(locations, listener, token, certPEM); err != nil { + logrus.WithError(err).WithField("path", path).Panic("Could not write gRPC service config file") + } else { + logrus.WithField("path", path).Info("Successfully saved gRPC service config file") + } + + s := &Service{ + grpcServer: grpc.NewServer( + grpc.Creds(credentials.NewTLS(tlsConfig)), + grpc.UnaryInterceptor(newUnaryTokenValidator(token)), + grpc.StreamInterceptor(newStreamTokenValidator(token)), + ), + listener: listener, + + panicHandler: panicHandler, + restarter: restarter, + bridge: bridge, log: logrus.WithField("pkg", "grpc"), initializing: sync.WaitGroup{}, initializationDone: sync.Once{}, firstTimeAutostart: sync.Once{}, - locations: locations, - token: uuid.NewString(), + + showOnStartup: showOnStartup, } - // Initializing.Done is only called sync.Once. Please keep the increment - // set to 1 + // Initializing.Done is only called sync.Once. Please keep the increment set to 1 s.initializing.Add(1) - tlsConfig, pemCert, err := s.generateTLSConfig() - if err != nil { - s.log.WithError(err).Panic("Could not generate gRPC TLS config") - } - - s.pemCert = string(pemCert) - + // Initialize the autostart. s.initAutostart() - s.grpcServer = grpc.NewServer( - grpc.Creds(credentials.NewTLS(tlsConfig)), - grpc.UnaryInterceptor(s.validateUnaryServerToken), - grpc.StreamInterceptor(s.validateStreamServerToken), - ) - RegisterBridgeServer(s.grpcServer, &s) - - s.listener, err = net.Listen("tcp", "127.0.0.1:0") // Port 0 means that the port is randomly picked by the system. - if err != nil { - s.log.WithError(err).Panic("Could not create gRPC listener") - } - - if path, err := s.saveGRPCServerConfigFile(); err != nil { - s.log.WithError(err).WithField("path", path).Panic("Could not write gRPC service config file") - } else { - s.log.WithField("path", path).Info("Successfully saved gRPC service config file") - } + // Register the gRPC service implementation. + RegisterBridgeServer(s.grpcServer, s) s.log.Info("gRPC server listening on ", s.listener.Addr()) - return &s + return s, nil } +// GODT-1507 Windows: autostart needs to be created after Qt is initialized. +// GODT-1206: if preferences file says it should be on enable it here. +// TO-DO GODT-1681 Autostart needs to be properly implement for gRPC approach. func (s *Service) initAutostart() { - // GODT-1507 Windows: autostart needs to be created after Qt is initialized. - // GODT-1206: if preferences file says it should be on enable it here. - - // TO-DO GODT-1681 Autostart needs to be properly implement for gRPC approach. - s.firstTimeAutostart.Do(func() { - shouldAutostartBeOn := s.bridge.GetBool(settings.AutostartKey) - if s.bridge.IsFirstStart() || shouldAutostartBeOn { - if err := s.bridge.EnableAutostart(); err != nil { + shouldAutostartBeOn := s.bridge.GetAutostart() + if s.bridge.GetFirstStart() || shouldAutostartBeOn { + if err := s.bridge.SetAutostart(true); err != nil { s.log.WithField("prefs", shouldAutostartBeOn).WithError(err).Error("Failed to enable first autostart") } return @@ -168,7 +152,7 @@ func (s *Service) initAutostart() { func (s *Service) Loop() error { defer func() { - s.bridge.SetBool(settings.FirstStartGUIKey, false) + _ = s.bridge.SetFirstStartGUI(false) }() go func() { @@ -179,7 +163,7 @@ func (s *Service) Loop() error { s.log.Info("Starting gRPC server") if err := s.grpcServer.Serve(s.listener); err != nil { - s.log.WithError(err).Error("Error serving gRPC") + s.log.WithError(err).Error("Failed to serve gRPC") return err } @@ -212,140 +196,59 @@ func (s *Service) WaitUntilFrontendIsReady() { s.initializing.Wait() } -func (s *Service) watchEvents() { // nolint:funlen - if s.bridge.HasError(bridge.ErrLocalCacheUnavailable) { - _ = s.SendEvent(NewCacheErrorEvent(CacheErrorType_CACHE_UNAVAILABLE_ERROR)) - } +func (s *Service) watchEvents() { + eventCh, done := s.bridge.GetEvents() + defer done() - errorCh := s.eventListener.ProvideChannel(events.ErrorEvent) - credentialsErrorCh := s.eventListener.ProvideChannel(events.CredentialsErrorEvent) - noActiveKeyForRecipientCh := s.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent) - internetConnChangedCh := s.eventListener.ProvideChannel(events.InternetConnChangedEvent) - secondInstanceCh := s.eventListener.ProvideChannel(events.SecondInstanceEvent) - restartBridgeCh := s.eventListener.ProvideChannel(events.RestartBridgeEvent) - addressChangedCh := s.eventListener.ProvideChannel(events.AddressChangedEvent) - addressChangedLogoutCh := s.eventListener.ProvideChannel(events.AddressChangedLogoutEvent) - logoutCh := s.eventListener.ProvideChannel(events.LogoutEvent) - updateApplicationCh := s.eventListener.ProvideChannel(events.UpgradeApplicationEvent) - userChangedCh := s.eventListener.ProvideChannel(events.UserRefreshEvent) - certIssue := s.eventListener.ProvideChannel(events.TLSCertIssue) - - // we forward events to the GUI/frontend via the gRPC event stream. - for { - select { - case errorDetails := <-errorCh: - if strings.Contains(errorDetails, "IMAP failed") { - _ = s.SendEvent(NewMailSettingsErrorEvent(MailSettingsErrorType_IMAP_PORT_ISSUE)) - } - if strings.Contains(errorDetails, "SMTP failed") { - _ = s.SendEvent(NewMailSettingsErrorEvent(MailSettingsErrorType_SMTP_PORT_ISSUE)) - } - case reason := <-credentialsErrorCh: - if reason == keychain.ErrMacKeychainRebuild.Error() { - _ = s.SendEvent(NewKeychainRebuildKeychainEvent()) - continue - } + // TODO: Better error events. + for _, err := range s.bridge.GetErrors() { + switch { + case errors.Is(err, vault.ErrCorrupt): _ = s.SendEvent(NewKeychainHasNoKeychainEvent()) - case email := <-noActiveKeyForRecipientCh: - _ = s.SendEvent(NewMailNoActiveKeyForRecipientEvent(email)) - case stat := <-internetConnChangedCh: - if stat == events.InternetOff { - _ = s.SendEvent(NewInternetStatusEvent(false)) - } - if stat == events.InternetOn { - _ = s.SendEvent(NewInternetStatusEvent(true)) - } - case <-secondInstanceCh: - _ = s.SendEvent(NewShowMainWindowEvent()) - case <-restartBridgeCh: - _, _ = s.Restart( - metadata.AppendToOutgoingContext(context.Background(), serverTokenMetadataKey, s.token), - &emptypb.Empty{}, - ) - case address := <-addressChangedCh: - _ = s.SendEvent(NewMailAddressChangeEvent(address)) - case address := <-addressChangedLogoutCh: - _ = s.SendEvent(NewMailAddressChangeLogoutEvent(address)) - case userID := <-logoutCh: - user, err := s.bridge.GetUserInfo(userID) - if err != nil { - return - } - _ = s.SendEvent(NewUserDisconnectedEvent(user.Username)) - case <-updateApplicationCh: - s.updateForce() - case userID := <-userChangedCh: - _ = s.SendEvent(NewUserChangedEvent(userID)) - case <-certIssue: - _ = s.SendEvent(NewMailApiCertIssue()) + case errors.Is(err, vault.ErrInsecure): + _ = s.SendEvent(NewKeychainHasNoKeychainEvent()) + + case errors.Is(err, bridge.ErrServeIMAP): + _ = s.SendEvent(NewMailSettingsErrorEvent(MailSettingsErrorType_IMAP_PORT_ISSUE)) + + case errors.Is(err, bridge.ErrServeSMTP): + _ = s.SendEvent(NewMailSettingsErrorEvent(MailSettingsErrorType_SMTP_PORT_ISSUE)) } } -} -func (s *Service) loginAbort() { - s.loginClean() -} + for event := range eventCh { + switch event := event.(type) { + case events.ConnStatus: + _ = s.SendEvent(NewInternetStatusEvent(event.Status == liteapi.StatusUp)) -func (s *Service) loginClean() { - s.auth = nil - s.authClient = nil - for i := range s.password { - s.password[i] = '\x00' - } - s.password = s.password[0:0] -} + case events.Raise: + _ = s.SendEvent(NewShowMainWindowEvent()) -func (s *Service) finishLogin() { - defer s.loginClean() + case events.UserAddressCreated: + _ = s.SendEvent(NewMailAddressChangeEvent(event.Address)) - if len(s.password) == 0 || s.auth == nil || s.authClient == nil { - s.log. - WithField("hasPass", len(s.password) != 0). - WithField("hasAuth", s.auth != nil). - WithField("hasClient", s.authClient != nil). - Error("Finish login: authentication incomplete") + case events.UserAddressChanged: + _ = s.SendEvent(NewMailAddressChangeEvent(event.Address)) - _ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, "Missing authentication, try again.")) - return - } + case events.UserAddressDeleted: + _ = s.SendEvent(NewMailAddressChangeLogoutEvent(event.Address)) - done := make(chan string) - s.eventListener.Add(events.UserChangeDone, done) - defer s.eventListener.Remove(events.UserChangeDone, done) + case events.UserChanged: + _ = s.SendEvent(NewUserChangedEvent(event.UserID)) - userID, err := s.bridge.FinishLogin(s.authClient, s.auth, s.password) - - if err != nil && err != users.ErrUserAlreadyConnected { - s.log.WithError(err).Errorf("Finish login failed") - _ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, err.Error())) - return - } - - // The user changed should be triggered by FinishLogin, but it is not - // guaranteed when this is going to happen. Therefor we should wait - // until we receive the signal from userChanged function. - s.waitForUserChangeDone(done, userID) - - s.log.WithField("userID", userID).Debug("Login finished") - _ = s.SendEvent(NewLoginFinishedEvent(userID)) - - if err == users.ErrUserAlreadyConnected { - s.log.WithError(err).Error("User already logged in") - _ = s.SendEvent(NewLoginAlreadyLoggedInEvent(userID)) - } -} - -func (s *Service) waitForUserChangeDone(done <-chan string, userID string) { - for { - select { - case changedID := <-done: - if changedID == userID { - return + case events.UserDeauth: + if user, err := s.bridge.GetUserInfo(event.UserID); err != nil { + s.log.WithError(err).Error("Failed to get user info") + } else { + _ = s.SendEvent(NewUserDisconnectedEvent(user.Username)) } - case <-time.After(2 * time.Second): - s.log.WithField("ID", userID).Warning("Login finished but user not added within 2 seconds") - return + + case events.TLSIssue: + _ = s.SendEvent(NewMailApiCertIssue()) + + case events.UpdateForced: + panic("TODO") } } } @@ -354,103 +257,46 @@ func (s *Service) triggerReset() { defer func() { _ = s.SendEvent(NewResetFinishedEvent()) }() - s.bridge.FactoryReset() + if err := s.bridge.FactoryReset(context.Background()); err != nil { + s.log.WithError(err).Error("Failed to reset") + } } -func (s *Service) checkUpdate() { - version, err := s.updater.Check() +func newTLSConfig() (*tls.Config, []byte, error) { + template, err := certs.NewTLSTemplate() if err != nil { - s.log.WithError(err).Error("An error occurred while checking for updates") - s.SetVersion(updater.VersionInfo{}) - return - } - s.SetVersion(version) -} - -func (s *Service) updateForce() { - s.updateCheckMutex.Lock() - defer s.updateCheckMutex.Unlock() - s.checkUpdate() - _ = s.SendEvent(NewUpdateForceEvent(s.newVersionInfo.Version.String())) -} - -func (s *Service) checkUpdateAndNotify(isReqFromUser bool) { - s.updateCheckMutex.Lock() - defer func() { - s.updateCheckMutex.Unlock() - _ = s.SendEvent(NewUpdateCheckFinishedEvent()) - }() - - s.checkUpdate() - version := s.newVersionInfo - if (version.Version == nil) || (version.Version.String() == "") { - if isReqFromUser { - _ = s.SendEvent(NewUpdateErrorEvent(UpdateErrorType_UPDATE_MANUAL_ERROR)) - } - return - } - if !s.updater.IsUpdateApplicable(s.newVersionInfo) { - s.log.Info("No need to update") - if isReqFromUser { - _ = s.SendEvent(NewUpdateIsLatestVersionEvent()) - } - } else if isReqFromUser { - s.NotifyManualUpdate(s.newVersionInfo, s.updater.CanInstall(s.newVersionInfo)) - } -} - -func (s *Service) installUpdate() { - s.updateCheckMutex.Lock() - defer s.updateCheckMutex.Unlock() - - if !s.updater.CanInstall(s.newVersionInfo) { - s.log.Warning("Skipping update installation, current version too old") - _ = s.SendEvent(NewUpdateErrorEvent(UpdateErrorType_UPDATE_MANUAL_ERROR)) - return + return nil, nil, fmt.Errorf("failed to create TLS template: %w", err) } - if err := s.updater.InstallUpdate(s.newVersionInfo); err != nil { - if errors.Cause(err) == updater.ErrDownloadVerify { - s.log.WithError(err).Warning("Skipping update installation due to temporary error") - } else { - s.log.WithError(err).Error("The update couldn't be installed") - _ = s.SendEvent(NewUpdateErrorEvent(UpdateErrorType_UPDATE_MANUAL_ERROR)) - } - return - } - - _ = s.SendEvent(NewUpdateSilentRestartNeededEvent()) -} - -func (s *Service) generateTLSConfig() (tlsConfig *cryptotls.Config, pemCert []byte, err error) { - pemCert, pemKey, err := tls.NewPEMKeyPair() + certPEM, keyPEM, err := certs.GenerateCert(template) if err != nil { - return nil, nil, errors.New("Could not get TLS config") + return nil, nil, fmt.Errorf("failed to generate cert: %w", err) } - tlsConfig, err = tls.GetConfigFromPEMKeyPair(pemCert, pemKey) + cert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { - return nil, nil, errors.New("Could not get TLS config") + return nil, nil, fmt.Errorf("failed to load cert: %w", err) } - tlsConfig.ClientAuth = cryptotls.NoClientCert // skip client auth if the certificate allow it. - - return tlsConfig, pemCert, nil + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.NoClientCert, + }, certPEM, nil } -func (s *Service) saveGRPCServerConfigFile() (string, error) { - address, ok := s.listener.Addr().(*net.TCPAddr) +func saveGRPCServerConfigFile(locations *locations.Locations, listener net.Listener, token string, certPEM []byte) (string, error) { + address, ok := listener.Addr().(*net.TCPAddr) if !ok { return "", fmt.Errorf("could not retrieve gRPC service listener address") } sc := config{ Port: address.Port, - Cert: s.pemCert, - Token: s.token, + Cert: string(certPEM), + Token: token, } - settingsPath, err := s.locations.ProvideSettingsPath() + settingsPath, err := locations.ProvideSettingsPath() if err != nil { return "", err } @@ -461,7 +307,7 @@ func (s *Service) saveGRPCServerConfigFile() (string, error) { } // validateServerToken verify that the server token provided by the client is valid. -func (s *Service) validateServerToken(ctx context.Context) error { +func validateServerToken(ctx context.Context, wantToken string) error { values, ok := metadata.FromIncomingContext(ctx) if !ok { return status.Error(codes.Unauthenticated, "missing server token") @@ -476,40 +322,31 @@ func (s *Service) validateServerToken(ctx context.Context) error { return status.Error(codes.Unauthenticated, "more than one server token was provided") } - if token[0] != s.token { + if token[0] != wantToken { return status.Error(codes.Unauthenticated, "invalid server token") } return nil } -// validateUnaryServerToken check the server token for every unary gRPC call. -func (s *Service) validateUnaryServerToken( - ctx context.Context, - req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, -) (resp interface{}, err error) { - if err := s.validateServerToken(ctx); err != nil { - return nil, err - } +// newUnaryTokenValidator checks the server token for every unary gRPC call. +func newUnaryTokenValidator(wantToken string) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if err := validateServerToken(ctx, wantToken); err != nil { + return nil, err + } - return handler(ctx, req) + return handler(ctx, req) + } } -// validateStreamServerToken check the server token for every gRPC stream request. -func (s *Service) validateStreamServerToken( - srv interface{}, - ss grpc.ServerStream, - info *grpc.StreamServerInfo, - handler grpc.StreamHandler, -) error { - logEntry := s.log.WithField("FullMethod", info.FullMethod) +// newStreamTokenValidator checks the server token for every gRPC stream request. +func newStreamTokenValidator(wantToken string) grpc.StreamServerInterceptor { + return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + if err := validateServerToken(stream.Context(), wantToken); err != nil { + return err + } - if err := s.validateServerToken(ss.Context()); err != nil { - logEntry.WithError(err).Error("Stream validator failed") - return err + return handler(srv, stream) } - - return handler(srv, ss) } diff --git a/internal/frontend/grpc/service_methods.go b/internal/frontend/grpc/service_methods.go index fde708a1..9eaaddff 100644 --- a/internal/frontend/grpc/service_methods.go +++ b/internal/frontend/grpc/service_methods.go @@ -23,15 +23,13 @@ import ( "runtime" "github.com/Masterminds/semver/v3" - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" "github.com/ProtonMail/proton-bridge/v2/internal/constants" "github.com/ProtonMail/proton-bridge/v2/internal/frontend/theme" "github.com/ProtonMail/proton-bridge/v2/internal/updater" "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" "github.com/ProtonMail/proton-bridge/v2/pkg/ports" "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" @@ -116,7 +114,7 @@ func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empt func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { s.log.Debug("Restart") - s.restarter.SetToRestart() + s.restarter.Set(true, false) return s.Quit(ctx, empty) } @@ -129,25 +127,19 @@ func (s *Service) ShowOnStartup(ctx context.Context, _ *emptypb.Empty) (*wrapper func (s *Service) ShowSplashScreen(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("ShowSplashScreen") - if s.bridge.IsFirstStart() { - return wrapperspb.Bool(false), nil - } - - ver, err := semver.NewVersion(s.bridge.GetLastVersion()) - if err != nil { - s.log.WithError(err).WithField("last", s.bridge.GetLastVersion()).Debug("Cannot parse last version") + if s.bridge.GetFirstStart() { return wrapperspb.Bool(false), nil } // Current splash screen contains update on rebranding. Therefore, it // should be shown only if the last used version was less than 2.2.0. - return wrapperspb.Bool(ver.LessThan(semver.MustParse("2.2.0"))), nil + return wrapperspb.Bool(s.bridge.GetLastVersion().LessThan(semver.MustParse("2.2.0"))), nil } func (s *Service) IsFirstGuiStart(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsFirstGuiStart") - return wrapperspb.Bool(s.bridge.GetBool(settings.FirstStartGUIKey)), nil + return wrapperspb.Bool(s.bridge.GetFirstStartGUI()), nil } func (s *Service) SetIsAutostartOn(ctx context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) { @@ -155,22 +147,16 @@ func (s *Service) SetIsAutostartOn(ctx context.Context, isOn *wrapperspb.BoolVal defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }() - if isOn.Value == s.bridge.IsAutostartEnabled() { + if isOn.Value == s.bridge.GetAutostart() { s.initAutostart() return &emptypb.Empty{}, nil } - var err error - if isOn.Value { - err = s.bridge.EnableAutostart() - } else { - err = s.bridge.DisableAutostart() - } - s.initAutostart() - if err != nil { + if err := s.bridge.SetAutostart(isOn.Value); err != nil { s.log.WithField("makeItEnabled", isOn.Value).WithError(err).Error("Autostart change failed") + return nil, status.Errorf(codes.Internal, "failed to set autostart: %v", err) } return &emptypb.Empty{}, nil @@ -179,7 +165,7 @@ func (s *Service) SetIsAutostartOn(ctx context.Context, isOn *wrapperspb.BoolVal func (s *Service) IsAutostartOn(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsAutostartOn") - return wrapperspb.Bool(s.bridge.IsAutostartEnabled()), nil + return wrapperspb.Bool(s.bridge.GetAutostart()), nil } func (s *Service) SetIsBetaEnabled(ctx context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) { @@ -190,8 +176,10 @@ func (s *Service) SetIsBetaEnabled(ctx context.Context, isEnabled *wrapperspb.Bo channel = updater.EarlyChannel } - s.bridge.SetUpdateChannel(channel) - s.checkUpdate() + if err := s.bridge.SetUpdateChannel(channel); err != nil { + s.log.WithError(err).Error("Failed to set update channel") + return nil, status.Errorf(codes.Internal, "failed to set update channel: %v", err) + } return &emptypb.Empty{}, nil } @@ -205,7 +193,10 @@ func (s *Service) IsBetaEnabled(ctx context.Context, _ *emptypb.Empty) (*wrapper func (s *Service) SetIsAllMailVisible(ctx context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) { s.log.WithField("isVisible", isVisible.Value).Debug("SetIsAllMailVisible") - s.bridge.SetIsAllMailVisible(isVisible.Value) + if err := s.bridge.SetShowAllMail(isVisible.Value); err != nil { + s.log.WithError(err).Error("Failed to set show all mail") + return nil, status.Errorf(codes.Internal, "failed to set show all mail: %v", err) + } return &emptypb.Empty{}, nil } @@ -213,7 +204,7 @@ func (s *Service) SetIsAllMailVisible(ctx context.Context, isVisible *wrapperspb func (s *Service) IsAllMailVisible(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsAllMailVisible") - return wrapperspb.Bool(s.bridge.IsAllMailVisible()), nil + return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil } func (s *Service) GoOs(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { @@ -241,7 +232,7 @@ func (s *Service) Version(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.St func (s *Service) LogsPath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("LogsPath") - path, err := s.bridge.ProvideLogsPath() + path, err := s.bridge.GetLogsPath() if err != nil { s.log.WithError(err).Error("Cannot determine logs path") return nil, err @@ -275,7 +266,10 @@ func (s *Service) SetColorSchemeName(ctx context.Context, name *wrapperspb.Strin return nil, status.Error(codes.NotFound, "Color scheme not available") } - s.bridge.Set(settings.ColorScheme, name.Value) + if err := s.bridge.SetColorScheme(name.Value); err != nil { + s.log.WithError(err).Error("Failed to set color scheme") + return nil, status.Errorf(codes.Internal, "failed to set color scheme: %v", err) + } return &emptypb.Empty{}, nil } @@ -283,10 +277,13 @@ func (s *Service) SetColorSchemeName(ctx context.Context, name *wrapperspb.Strin func (s *Service) ColorSchemeName(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("ColorSchemeName") - current := s.bridge.Get(settings.ColorScheme) + current := s.bridge.GetColorScheme() if !theme.IsAvailable(theme.Theme(current)) { current = string(theme.DefaultTheme()) - s.bridge.Set(settings.ColorScheme, current) + if err := s.bridge.SetColorScheme(current); err != nil { + s.log.WithError(err).Error("Failed to set color scheme") + return nil, status.Errorf(codes.Internal, "failed to set color scheme: %v", err) + } } return wrapperspb.String(current), nil @@ -312,6 +309,7 @@ func (s *Service) ReportBug(ctx context.Context, report *ReportBugRequest) (*emp defer func() { _ = s.SendEvent(NewReportBugFinishedEvent()) }() if err := s.bridge.ReportBug( + context.Background(), report.OsType, report.OsVersion, report.Description, @@ -331,6 +329,7 @@ func (s *Service) ReportBug(ctx context.Context, report *ReportBugRequest) (*emp return &emptypb.Empty{}, nil } +/* func (s *Service) ForceLauncher(ctx context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) { s.log.WithField("launcher", launcher.Value).Debug("ForceLauncher") @@ -350,6 +349,7 @@ func (s *Service) SetMainExecutable(ctx context.Context, exe *wrapperspb.StringV }() return &emptypb.Empty{}, nil } +*/ func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) { s.log.WithField("username", login.Username).Debug("Login") @@ -357,135 +357,44 @@ func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empt go func() { defer s.panicHandler.HandlePanic() - var err error - s.password, err = base64.StdEncoding.DecodeString(login.Password) + password, err := base64.StdEncoding.DecodeString(login.Password) if err != nil { s.log.WithError(err).Error("Cannot decode password") _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode password")) - s.loginClean() return } - s.authClient, s.auth, err = s.bridge.Login(login.Username, s.password) + // TODO: Handle different error types! + // - bad credentials + // - bad proton plan + // - user already exists + userID, err := s.bridge.LoginUser(context.Background(), login.Username, string(password), nil, nil) if err != nil { - if err == pmapi.ErrPasswordWrong { - // Remove error message since it is hardcoded in QML. - _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "")) - s.loginClean() - return - } - if err == pmapi.ErrPaidPlanRequired { - _ = s.SendEvent(NewLoginError(LoginErrorType_FREE_USER, "")) - s.loginClean() - return - } - _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error())) - s.loginClean() + s.log.WithError(err).Error("Cannot login user") + _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot login user")) return } - if s.auth.HasTwoFactor() { - _ = s.SendEvent(NewLoginTfaRequestedEvent(login.Username)) - return - } - if s.auth.HasMailboxPassword() { - _ = s.SendEvent(NewLoginTwoPasswordsRequestedEvent()) - return - } - - s.finishLogin() - }() - return &emptypb.Empty{}, nil -} - -func (s *Service) Login2FA(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) { - s.log.WithField("username", login.Username).Debug("Login2FA") - - go func() { - defer s.panicHandler.HandlePanic() - - if s.auth == nil || s.authClient == nil { - s.log.Errorf("Login 2FA: authethication incomplete %p %p", s.auth, s.authClient) - _ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, "Missing authentication, try again.")) - s.loginClean() - return - } - - twoFA, err := base64.StdEncoding.DecodeString(login.Password) - if err != nil { - s.log.WithError(err).Error("Cannot decode 2fa code") - _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode 2fa code")) - s.loginClean() - return - } - - err = s.authClient.Auth2FA(context.Background(), string(twoFA)) - if err == pmapi.ErrBad2FACodeTryAgain { - s.log.Warn("Login 2FA: retry 2fa") - _ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ERROR, "")) - return - } - - if err == pmapi.ErrBad2FACode { - s.log.Warn("Login 2FA: abort 2fa") - _ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, "")) - s.loginClean() - return - } - - if err != nil { - s.log.WithError(err).Warn("Login 2FA: failed.") - _ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, err.Error())) - s.loginClean() - return - } - - if s.auth.HasMailboxPassword() { - _ = s.SendEvent(NewLoginTwoPasswordsRequestedEvent()) - return - } - - s.finishLogin() + _ = s.SendEvent(NewLoginFinishedEvent(userID)) }() return &emptypb.Empty{}, nil } -func (s *Service) Login2Passwords(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) { - s.log.WithField("username", login.Username).Debug("Login2Passwords") - - go func() { - defer s.panicHandler.HandlePanic() - - var err error - s.password, err = base64.StdEncoding.DecodeString(login.Password) - - if err != nil { - s.log.WithError(err).Error("Cannot decode mbox password") - _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode mbox password")) - s.loginClean() - return - } - - s.finishLogin() - }() - - return &emptypb.Empty{}, nil +func (s *Service) Login2FA(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) { + panic("TODO") } -func (s *Service) LoginAbort(ctx context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) { - s.log.WithField("username", loginAbort.Username).Debug("LoginAbort") - - go func() { - defer s.panicHandler.HandlePanic() - - s.loginAbort() - }() - - return &emptypb.Empty{}, nil +func (s *Service) Login2Passwords(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) { + panic("TODO") } -func (s *Service) CheckUpdate(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { +func (s *Service) LoginAbort(_ context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) { + panic("TODO") +} + +/* +func (s *Service) CheckUpdate(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { s.log.Debug("CheckUpdate") go func() { @@ -507,21 +416,20 @@ func (s *Service) InstallUpdate(ctx context.Context, _ *emptypb.Empty) (*emptypb return &emptypb.Empty{}, nil } +*/ func (s *Service) SetIsAutomaticUpdateOn(ctx context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) { s.log.WithField("isOn", isOn.Value).Debug("SetIsAutomaticUpdateOn") - currentlyOn := s.bridge.GetBool(settings.AutoUpdateKey) + currentlyOn := s.bridge.GetAutoUpdate() if currentlyOn == isOn.Value { return &emptypb.Empty{}, nil } - s.bridge.SetBool(settings.AutoUpdateKey, isOn.Value) - go func() { - defer s.panicHandler.HandlePanic() - - s.checkUpdateAndNotify(false) - }() + if err := s.bridge.SetAutoUpdate(isOn.Value); err != nil { + s.log.WithError(err).Error("Failed to set auto update") + return nil, status.Errorf(codes.Internal, "failed to set auto update: %v", err) + } return &emptypb.Empty{}, nil } @@ -529,51 +437,21 @@ func (s *Service) SetIsAutomaticUpdateOn(ctx context.Context, isOn *wrapperspb.B func (s *Service) IsAutomaticUpdateOn(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsAutomaticUpdateOn") - return wrapperspb.Bool(s.bridge.GetBool(settings.AutoUpdateKey)), nil -} - -func (s *Service) IsCacheOnDiskEnabled(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { - s.log.Debug("IsCacheOnDiskEnabled") - - return wrapperspb.Bool(s.bridge.GetBool(settings.CacheEnabledKey)), nil + return wrapperspb.Bool(s.bridge.GetAutoUpdate()), nil } func (s *Service) DiskCachePath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("DiskCachePath") - return wrapperspb.String(s.bridge.Get(settings.CacheLocationKey)), nil + return wrapperspb.String(s.bridge.GetGluonDir()), nil } func (s *Service) ChangeLocalCache(ctx context.Context, change *ChangeLocalCacheRequest) (*emptypb.Empty, error) { - s.log.WithField("enableDiskCache", change.EnableDiskCache). - WithField("diskCachePath", change.DiskCachePath). - Debug("DiskCachePath") + s.log.WithField("diskCachePath", change.DiskCachePath).Debug("DiskCachePath") - restart := false - defer func(willRestart *bool) { - _ = s.SendEvent(NewCacheChangeLocalCacheFinishedEvent(*willRestart)) - if *willRestart { - _, _ = s.Restart(ctx, &emptypb.Empty{}) - } - }(&restart) - - if change.EnableDiskCache != s.bridge.GetBool(settings.CacheEnabledKey) { - if change.EnableDiskCache { - if err := s.bridge.EnableCache(); err != nil { - s.log.WithError(err).Error("Cannot enable disk cache") - } else { - restart = true - _ = s.SendEvent(NewIsCacheOnDiskEnabledChanged(s.bridge.GetBool(settings.CacheEnabledKey))) - } - } else { - if err := s.bridge.DisableCache(); err != nil { - s.log.WithError(err).Error("Cannot disable disk cache") - } else { - restart = true - _ = s.SendEvent(NewIsCacheOnDiskEnabledChanged(s.bridge.GetBool(settings.CacheEnabledKey))) - } - } - } + defer func() { + _ = s.SendEvent(NewCacheChangeLocalCacheFinishedEvent(false)) + }() path := change.DiskCachePath //goland:noinspection GoBoolExpressions @@ -581,16 +459,14 @@ func (s *Service) ChangeLocalCache(ctx context.Context, change *ChangeLocalCache path = path[1:] } - if change.EnableDiskCache && path != s.bridge.Get(settings.CacheLocationKey) { - if err := s.bridge.MigrateCache(s.bridge.Get(settings.CacheLocationKey), path); err != nil { + if path != s.bridge.GetGluonDir() { + if err := s.bridge.SetGluonDir(ctx, path); err != nil { s.log.WithError(err).Error("The local cache location could not be changed.") _ = s.SendEvent(NewCacheErrorEvent(CacheErrorType_CACHE_CANT_MOVE_ERROR)) return &emptypb.Empty{}, nil } - s.bridge.Set(settings.CacheLocationKey, path) - restart = true - _ = s.SendEvent(NewDiskCachePathChanged(s.bridge.Get(settings.CacheLocationKey))) + _ = s.SendEvent(NewDiskCachePathChanged(s.bridge.GetGluonDir())) } _ = s.SendEvent(NewCacheLocationChangeSuccessEvent()) @@ -601,7 +477,10 @@ func (s *Service) ChangeLocalCache(ctx context.Context, change *ChangeLocalCache func (s *Service) SetIsDoHEnabled(ctx context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) { s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsDohEnabled") - s.bridge.SetProxyAllowed(isEnabled.Value) + if err := s.bridge.SetProxyAllowed(isEnabled.Value); err != nil { + s.log.WithError(err).Error("Failed to set DoH") + return nil, status.Errorf(codes.Internal, "failed to set DoH: %v", err) + } return &emptypb.Empty{}, nil } @@ -615,13 +494,14 @@ func (s *Service) IsDoHEnabled(ctx context.Context, _ *emptypb.Empty) (*wrappers func (s *Service) SetUseSslForSmtp(ctx context.Context, useSsl *wrapperspb.BoolValue) (*emptypb.Empty, error) { //nolint:revive,stylecheck s.log.WithField("useSsl", useSsl.Value).Debug("SetUseSslForSmtp") - if s.bridge.GetBool(settings.SMTPSSLKey) == useSsl.Value { + if s.bridge.GetSMTPSSL() == useSsl.Value { return &emptypb.Empty{}, nil } - s.bridge.SetBool(settings.SMTPSSLKey, useSsl.Value) - - defer func() { _, _ = s.Restart(ctx, &emptypb.Empty{}) }() + if err := s.bridge.SetSMTPSSL(useSsl.Value); err != nil { + s.log.WithError(err).Error("Failed to set SMTP SSL") + return nil, status.Errorf(codes.Internal, "failed to set SMTP SSL: %v", err) + } return &emptypb.Empty{}, s.SendEvent(NewMailSettingsUseSslForSmtpFinishedEvent()) } @@ -629,34 +509,39 @@ func (s *Service) SetUseSslForSmtp(ctx context.Context, useSsl *wrapperspb.BoolV func (s *Service) UseSslForSmtp(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { //nolint:revive,stylecheck s.log.Debug("UseSslForSmtp") - return wrapperspb.Bool(s.bridge.GetBool(settings.SMTPSSLKey)), nil + return wrapperspb.Bool(s.bridge.GetSMTPSSL()), nil } func (s *Service) Hostname(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("Hostname") - return wrapperspb.String(bridge.Host), nil + return wrapperspb.String(constants.Host), nil } func (s *Service) ImapPort(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.Int32Value, error) { s.log.Debug("ImapPort") - return wrapperspb.Int32(int32(s.bridge.GetInt(settings.IMAPPortKey))), nil + return wrapperspb.Int32(int32(s.bridge.GetIMAPPort())), nil } func (s *Service) SmtpPort(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.Int32Value, error) { //nolint:revive,stylecheck s.log.Debug("SmtpPort") - return wrapperspb.Int32(int32(s.bridge.GetInt(settings.SMTPPortKey))), nil + return wrapperspb.Int32(int32(s.bridge.GetSMTPPort())), nil } func (s *Service) ChangePorts(ctx context.Context, ports *ChangePortsRequest) (*emptypb.Empty, error) { s.log.WithField("imapPort", ports.ImapPort).WithField("smtpPort", ports.SmtpPort).Debug("ChangePorts") - s.bridge.SetInt(settings.IMAPPortKey, int(ports.ImapPort)) - s.bridge.SetInt(settings.SMTPPortKey, int(ports.SmtpPort)) + if err := s.bridge.SetIMAPPort(int(ports.ImapPort)); err != nil { + s.log.WithError(err).Error("Failed to set IMAP port") + return nil, status.Errorf(codes.Internal, "failed to set IMAP port: %v", err) + } - defer func() { _, _ = s.Restart(ctx, &emptypb.Empty{}) }() + if err := s.bridge.SetSMTPPort(int(ports.SmtpPort)); err != nil { + s.log.WithError(err).Error("Failed to set SMTP port") + return nil, status.Errorf(codes.Internal, "failed to set SMTP port: %v", err) + } return &emptypb.Empty{}, s.SendEvent(NewMailSettingsChangePortFinishedEvent()) } @@ -670,12 +555,7 @@ func (s *Service) IsPortFree(ctx context.Context, port *wrapperspb.Int32Value) ( func (s *Service) AvailableKeychains(ctx context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) { s.log.Debug("AvailableKeychains") - keychains := make([]string, 0, len(keychain.Helpers)) - for chain := range keychain.Helpers { - keychains = append(keychains, chain) - } - - return &AvailableKeychainsResponse{Keychains: keychains}, nil + return &AvailableKeychainsResponse{Keychains: maps.Keys(keychain.Helpers)}, nil } func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) { @@ -684,11 +564,20 @@ func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.S defer func() { _, _ = s.Restart(ctx, &emptypb.Empty{}) }() defer func() { _ = s.SendEvent(NewKeychainChangeKeychainFinishedEvent()) }() - if s.bridge.GetKeychainApp() == keychain.Value { + helper, err := s.bridge.GetKeychainApp() + if err != nil { + s.log.WithError(err).Error("Failed to get current keychain") + return nil, status.Errorf(codes.Internal, "failed to get current keychain: %v", err) + } + + if helper == keychain.Value { return &emptypb.Empty{}, nil } - s.bridge.SetKeychainApp(keychain.Value) + if err := s.bridge.SetKeychainApp(keychain.Value); err != nil { + s.log.WithError(err).Error("Failed to set keychain") + return nil, status.Errorf(codes.Internal, "failed to set keychain: %v", err) + } return &emptypb.Empty{}, nil } @@ -696,5 +585,11 @@ func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.S func (s *Service) CurrentKeychain(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("CurrentKeychain") - return wrapperspb.String(s.bridge.GetKeychainApp()), nil + helper, err := s.bridge.GetKeychainApp() + if err != nil { + s.log.WithError(err).Error("Failed to get current keychain") + return nil, status.Errorf(codes.Internal, "failed to get current keychain: %v", err) + } + + return wrapperspb.String(helper), nil } diff --git a/internal/frontend/grpc/service_stream.go b/internal/frontend/grpc/service_stream.go index 6ab46b0f..f32511fd 100644 --- a/internal/frontend/grpc/service_stream.go +++ b/internal/frontend/grpc/service_stream.go @@ -87,12 +87,8 @@ func (s *Service) StopEventStream(ctx context.Context, _ *emptypb.Empty) (*empty // SendEvent sends an event to the via the gRPC event stream. func (s *Service) SendEvent(event *StreamEvent) error { - s.eventQueueMutex.Lock() - defer s.eventQueueMutex.Unlock() - - if s.eventStreamCh == nil { - // nobody is connected to the event stream, we queue events - s.eventQueue = append(s.eventQueue, event) + if s.eventStreamCh == nil { // nobody is connected to the event stream, we queue events + s.queueEvent(event) return nil } @@ -167,3 +163,14 @@ func (s *Service) StartEventTest() error { //nolint:funlen return nil } + +func (s *Service) queueEvent(event *StreamEvent) { + s.eventQueueMutex.Lock() + defer s.eventQueueMutex.Unlock() + + if event.isInternetStatus() { + s.eventQueue = append(filterOutInternetStatusEvents(s.eventQueue), event) + } else { + s.eventQueue = append(s.eventQueue, event) + } +} diff --git a/internal/frontend/grpc/service_updates.go b/internal/frontend/grpc/service_updates.go new file mode 100644 index 00000000..7b7575ab --- /dev/null +++ b/internal/frontend/grpc/service_updates.go @@ -0,0 +1,68 @@ +package grpc + +/* +func (s *Service) checkUpdate() { + version, err := s.updater.Check() + if err != nil { + s.log.WithError(err).Error("An error occurred while checking for updates") + s.SetVersion(updater.VersionInfo{}) + return + } + s.SetVersion(version) +} + +func (s *Service) updateForce() { + s.updateCheckMutex.Lock() + defer s.updateCheckMutex.Unlock() + s.checkUpdate() + _ = s.SendEvent(NewUpdateForceEvent(s.newVersionInfo.Version.String())) +} + +func (s *Service) checkUpdateAndNotify(isReqFromUser bool) { + s.updateCheckMutex.Lock() + defer func() { + s.updateCheckMutex.Unlock() + _ = s.SendEvent(NewUpdateCheckFinishedEvent()) + }() + + s.checkUpdate() + version := s.newVersionInfo + if version.Version.String() == "" { + if isReqFromUser { + _ = s.SendEvent(NewUpdateErrorEvent(UpdateErrorType_UPDATE_MANUAL_ERROR)) + } + return + } + if !s.updater.IsUpdateApplicable(s.newVersionInfo) { + s.log.Info("No need to update") + if isReqFromUser { + _ = s.SendEvent(NewUpdateIsLatestVersionEvent()) + } + } else if isReqFromUser { + s.NotifyManualUpdate(s.newVersionInfo, s.updater.CanInstall(s.newVersionInfo)) + } +} + +func (s *Service) installUpdate() { + s.updateCheckMutex.Lock() + defer s.updateCheckMutex.Unlock() + + if !s.updater.CanInstall(s.newVersionInfo) { + s.log.Warning("Skipping update installation, current version too old") + _ = s.SendEvent(NewUpdateErrorEvent(UpdateErrorType_UPDATE_MANUAL_ERROR)) + return + } + + if err := s.updater.InstallUpdate(s.newVersionInfo); err != nil { + if errors.Cause(err) == updater.ErrDownloadVerify { + s.log.WithError(err).Warning("Skipping update installation due to temporary error") + } else { + s.log.WithError(err).Error("The update couldn't be installed") + _ = s.SendEvent(NewUpdateErrorEvent(UpdateErrorType_UPDATE_MANUAL_ERROR)) + } + return + } + + _ = s.SendEvent(NewUpdateSilentRestartNeededEvent()) +} +*/ diff --git a/internal/frontend/grpc/service_user.go b/internal/frontend/grpc/service_user.go index 9ccd1f61..d6fadb7a 100644 --- a/internal/frontend/grpc/service_user.go +++ b/internal/frontend/grpc/service_user.go @@ -19,9 +19,8 @@ package grpc import ( "context" - "time" - "github.com/ProtonMail/proton-bridge/v2/internal/users" + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -75,15 +74,15 @@ func (s *Service) SetUserSplitMode(ctx context.Context, splitMode *UserSplitMode defer s.panicHandler.HandlePanic() defer func() { _ = s.SendEvent(NewUserToggleSplitModeFinishedEvent(splitMode.UserID)) }() - var targetMode users.AddressMode + var targetMode bridge.AddressMode - if splitMode.Active && user.Mode == users.CombinedMode { - targetMode = users.SplitMode - } else if !splitMode.Active && user.Mode == users.SplitMode { - targetMode = users.CombinedMode + if splitMode.Active && user.AddressMode == bridge.CombinedMode { + targetMode = bridge.SplitMode + } else if !splitMode.Active && user.AddressMode == bridge.SplitMode { + targetMode = bridge.CombinedMode } - if err := s.bridge.SetAddressMode(user.ID, targetMode); err != nil { + if err := s.bridge.SetAddressMode(user.UserID, targetMode); err != nil { logrus.WithError(err).Error("Failed to set address mode") } }() @@ -101,7 +100,7 @@ func (s *Service) LogoutUser(ctx context.Context, userID *wrapperspb.StringValue go func() { defer s.panicHandler.HandlePanic() - if err := s.bridge.LogoutUser(userID.Value); err != nil { + if err := s.bridge.LogoutUser(context.Background(), userID.Value); err != nil { logrus.WithError(err).Error("Failed to log user out") } }() @@ -116,7 +115,7 @@ func (s *Service) RemoveUser(ctx context.Context, userID *wrapperspb.StringValue defer s.panicHandler.HandlePanic() // remove preferences - if err := s.bridge.DeleteUser(userID.Value, false); err != nil { + if err := s.bridge.DeleteUser(context.Background(), userID.Value); err != nil { s.log.WithError(err).Error("Failed to remove user") // notification } @@ -127,18 +126,10 @@ func (s *Service) RemoveUser(ctx context.Context, userID *wrapperspb.StringValue func (s *Service) ConfigureUserAppleMail(ctx context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) { s.log.WithField("UserID", request.UserID).WithField("Address", request.Address).Debug("ConfigureUserAppleMail") - restart, err := s.bridge.ConfigureAppleMail(request.UserID, request.Address) - if err != nil { + if err := s.bridge.ConfigureAppleMail(request.UserID, request.Address); err != nil { s.log.WithField("userID", request.UserID).Error("Cannot configure AppleMail for user") return nil, status.Error(codes.Internal, "Apple Mail config failed") } - // There is delay needed for external window to open. - if restart { - s.log.Warn("Detected Catalina or newer with bad SMTP SSL settings, now using SSL, bridge needs to restart") - time.Sleep(2 * time.Second) - return s.Restart(ctx, &emptypb.Empty{}) - } - return &emptypb.Empty{}, nil } diff --git a/internal/frontend/grpc/utils.go b/internal/frontend/grpc/utils.go index a1f29767..b8dbb580 100644 --- a/internal/frontend/grpc/utils.go +++ b/internal/frontend/grpc/utils.go @@ -21,7 +21,7 @@ import ( "regexp" "strings" - "github.com/ProtonMail/proton-bridge/v2/internal/users" + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/sirupsen/logrus" ) @@ -58,17 +58,17 @@ func getInitials(fullName string) string { } // grpcUserFromInfo converts a bridge user to a gRPC user. -func grpcUserFromInfo(user users.UserInfo) *User { +func grpcUserFromInfo(user bridge.UserInfo) *User { return &User{ - Id: user.ID, + Id: user.UserID, Username: user.Username, AvatarText: getInitials(user.Username), LoggedIn: user.Connected, - SplitMode: user.Mode == users.SplitMode, + SplitMode: user.AddressMode == bridge.SplitMode, SetupGuideSeen: true, // users listed have already seen the setup guide. - UsedBytes: user.UsedBytes, - TotalBytes: user.TotalBytes, - Password: user.Password, + UsedBytes: int64(user.UsedSpace), + TotalBytes: int64(user.MaxSpace), + Password: user.BridgePass, Addresses: user.Addresses, } } diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go deleted file mode 100644 index 76ca5abc..00000000 --- a/internal/frontend/types/types.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package types provides interfaces used in frontend packages. -package types - -import ( - "crypto/tls" - - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/updater" - "github.com/ProtonMail/proton-bridge/v2/internal/users" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -// PanicHandler is an interface of a type that can be used to gracefully handle panics which occur. -type PanicHandler interface { - HandlePanic() -} - -// Restarter allows the app to set itself to restart next time it is closed. -type Restarter interface { - SetToRestart() - ForceLauncher(string) - SetMainExecutable(string) -} - -type Updater interface { - Check() (updater.VersionInfo, error) - InstallUpdate(updater.VersionInfo) error - IsUpdateApplicable(updater.VersionInfo) bool - CanInstall(updater.VersionInfo) bool -} - -// Bridger is an interface of bridge needed by frontend. -type Bridger interface { - Login(username string, password []byte) (pmapi.Client, *pmapi.Auth, error) - FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (string, error) - - GetUserIDs() []string - GetUserInfo(string) (users.UserInfo, error) - LogoutUser(userID string) error - DeleteUser(userID string, clearCache bool) error - SetAddressMode(userID string, split users.AddressMode) error - - ClearData() error - ClearUsers() error - FactoryReset() - - GetTLSConfig() (*tls.Config, error) - ProvideLogsPath() (string, error) - GetLicenseFilePath() string - GetDependencyLicensesLink() string - - GetCurrentUserAgent() string - SetCurrentPlatform(string) - - Get(settings.Key) string - Set(settings.Key, string) - GetBool(settings.Key) bool - SetBool(settings.Key, bool) - GetInt(settings.Key) int - SetInt(settings.Key, int) - - ConfigureAppleMail(userID, address string) (bool, error) - - // -- old -- - - ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error - SetProxyAllowed(bool) - GetProxyAllowed() bool - EnableCache() error - DisableCache() error - MigrateCache(from, to string) error - GetUpdateChannel() updater.UpdateChannel - SetUpdateChannel(updater.UpdateChannel) - GetKeychainApp() string - SetKeychainApp(keychain string) - HasError(err error) bool - IsAutostartEnabled() bool - EnableAutostart() error - DisableAutostart() error - GetLastVersion() string - IsFirstStart() bool - IsAllMailVisible() bool - SetIsAllMailVisible(bool) -} diff --git a/internal/imap/backend.go b/internal/imap/backend.go deleted file mode 100644 index 5f93a046..00000000 --- a/internal/imap/backend.go +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package imap provides IMAP server of the Bridge. -// -// Methods are called by the go-imap library in parallel. -// Additional parallelism is achieved while handling each IMAP request. -// -// For example, ListMessages internally uses `fetchWorkers` workers to resolve each requested item. -// When IMAP clients request message literals (or parts thereof), we sometimes need to build RFC822 message literals. -// To do this, we pass build jobs to the message builder, which internally manages its own parallelism. -// Summary: -// - each IMAP fetch request is handled in parallel, -// - within each IMAP fetch request, individual items are handled by a pool of `fetchWorkers` workers, -// - within each worker, build jobs are posted to the message builder, -// - the message builder handles build jobs using its own, independent worker pool, -// -// The builder will handle jobs in parallel up to its own internal limit. This prevents it from overwhelming API. -package imap - -import ( - "strings" - "sync" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/users" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/emersion/go-imap" - goIMAPBackend "github.com/emersion/go-imap/backend" -) - -type panicHandler interface { - HandlePanic() -} - -type imapBackend struct { - panicHandler panicHandler - bridge bridger - updates *imapUpdates - eventListener listener.Listener - listWorkers int - - users map[string]*imapUser - usersLocker sync.Locker - - imapCache map[string]map[string]string - imapCachePath string - imapCacheLock *sync.RWMutex -} - -type settingsProvider interface { - GetInt(settings.Key) int -} - -// NewIMAPBackend returns struct implementing go-imap/backend interface. -func NewIMAPBackend( - panicHandler panicHandler, - eventListener listener.Listener, - cache cacheProvider, - setting settingsProvider, - bridge *bridge.Bridge, -) *imapBackend { //nolint:revive - bridgeWrap := newBridgeWrap(bridge) - - imapWorkers := setting.GetInt(settings.IMAPWorkers) - backend := newIMAPBackend(panicHandler, cache, bridgeWrap, eventListener, imapWorkers) - - go backend.monitorDisconnectedUsers() - - return backend -} - -func newIMAPBackend( - panicHandler panicHandler, - cache cacheProvider, - bridge bridger, - eventListener listener.Listener, - listWorkers int, -) *imapBackend { - ib := &imapBackend{ - panicHandler: panicHandler, - bridge: bridge, - eventListener: eventListener, - - users: map[string]*imapUser{}, - usersLocker: &sync.Mutex{}, - - imapCachePath: cache.GetIMAPCachePath(), - imapCacheLock: &sync.RWMutex{}, - listWorkers: listWorkers, - } - ib.updates = newIMAPUpdates(ib) - return ib -} - -func (ib *imapBackend) getUser(address string) (*imapUser, error) { - ib.usersLocker.Lock() - defer ib.usersLocker.Unlock() - - address = strings.ToLower(address) - imapUser, ok := ib.users[address] - if ok { - return imapUser, nil - } - return ib.createUser(address) -} - -// createUser require that address MUST be in lowercase. -func (ib *imapBackend) createUser(address string) (*imapUser, error) { - log.WithField("address", address).Debug("Creating new IMAP user") - - user, err := ib.bridge.GetUser(address) - if err != nil { - return nil, err - } - - // Make sure you return the same user for all valid addresses when in combined mode. - if user.IsCombinedAddressMode() { - address = strings.ToLower(user.GetPrimaryAddress()) - if combinedUser, ok := ib.users[address]; ok { - return combinedUser, nil - } - } - - // Client can log in only using address so we can properly close all IMAP connections. - var addressID string - if addressID, err = user.GetAddressID(address); err != nil { - return nil, err - } - - newUser, err := newIMAPUser(ib.panicHandler, ib, user, addressID, address) - if err != nil { - return nil, err - } - - ib.users[address] = newUser - - return newUser, nil -} - -// deleteUser removes a user from the users map. -// This is a safe operation even if the user doesn't exist so it is no problem if it is done twice. -func (ib *imapBackend) deleteUser(address string) { - log.WithField("address", address).Debug("Deleting IMAP user") - - ib.usersLocker.Lock() - defer ib.usersLocker.Unlock() - - delete(ib.users, strings.ToLower(address)) -} - -// Login authenticates a user. -func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMAPBackend.User, error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer ib.panicHandler.HandlePanic() - - if ib.bridge.HasError(bridge.ErrLocalCacheUnavailable) { - return nil, users.ErrLoggedOutUser - } - - imapUser, err := ib.getUser(username) - if err != nil { - log.WithError(err).Warn("Cannot get user") - return nil, err - } - - if err := imapUser.user.CheckBridgeLogin(password); err != nil { - log.WithError(err).Error("Could not check bridge password") - if err := imapUser.Logout(); err != nil { - log.WithError(err).Warn("Could not logout user after unsuccessful login check") - } - // Apple Mail sometimes generates a lot of requests very quickly. - // It's therefore good to have a timeout after a bad login so that we can slow - // those requests down a little bit. - time.Sleep(10 * time.Second) - return nil, err - } - - // The update channel should be nil until we try to login to IMAP for the first time - // so that it doesn't make bridge slow for users who are only using bridge for SMTP - // (otherwise the store will be locked for 1 sec per email during synchronization). - if store := imapUser.user.GetStore(); store != nil { - store.SetChangeNotifier(ib.updates) - } - - return imapUser, nil -} - -// Updates returns a channel of updates for IMAP IDLE extension. -func (ib *imapBackend) Updates() <-chan goIMAPBackend.Update { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer ib.panicHandler.HandlePanic() - - return ib.updates.ch -} - -func (ib *imapBackend) CreateMessageLimit() *uint32 { - return nil -} - -// monitorDisconnectedUsers removes users when it receives a close connection event for them. -func (ib *imapBackend) monitorDisconnectedUsers() { - ch := make(chan string) - ib.eventListener.Add(events.CloseConnectionEvent, ch) - - for address := range ch { - // delete the user to ensure future imap login attempts use the latest bridge user - // (bridge user might be removed-readded so we want to use the new bridge user object). - ib.deleteUser(address) - } -} diff --git a/internal/imap/backend_cache.go b/internal/imap/backend_cache.go deleted file mode 100644 index bdf16d08..00000000 --- a/internal/imap/backend_cache.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "encoding/json" - "errors" - "os" - "strings" -) - -// Cache keys. -const ( - SubscriptionException = "subscription_exceptions" -) - -// addToCache adds item to existing item list. -// Starting from following structure: -// -// { -// "username": {"label": "item1;item2"} -// } -// -// After calling addToCache("username", "label", "newItem") we get: -// -// { -// "username": {"label": "item1;item2;newItem"} -// } -func (ib *imapBackend) addToCache(userID, label, toAdd string) { - list := ib.getCacheList(userID, label) - - if list != "" { - list = list + ";" + toAdd - } else { - list = toAdd - } - - ib.imapCacheLock.Lock() - ib.imapCache[userID][label] = list - ib.imapCacheLock.Unlock() - - if err := ib.saveIMAPCache(); err != nil { - log.Info("Backend/userinfo: could not save cache: ", err) - } -} - -func (ib *imapBackend) removeFromCache(userID, label, toRemove string) { - list := ib.getCacheList(userID, label) - - split := strings.Split(list, ";") - - for i, item := range split { - if item == toRemove { - split = append(split[:i], split[i+1:]...) - } - } - - ib.imapCacheLock.Lock() - ib.imapCache[userID][label] = strings.Join(split, ";") - ib.imapCacheLock.Unlock() - - if err := ib.saveIMAPCache(); err != nil { - log.Info("Backend/userinfo: could not save cache: ", err) - } -} - -func (ib *imapBackend) getCacheList(userID, label string) (list string) { - if err := ib.loadIMAPCache(); err != nil { - log.WithError(err).Warn("Could not load cache") - } - - ib.imapCacheLock.Lock() - if ib.imapCache == nil { - ib.imapCache = map[string]map[string]string{} - } - - if ib.imapCache[userID] == nil { - ib.imapCache[userID] = map[string]string{} - ib.imapCache[userID][SubscriptionException] = "" - } - - list = ib.imapCache[userID][label] - - ib.imapCacheLock.Unlock() - - if err := ib.saveIMAPCache(); err != nil { - log.WithError(err).Warn("Could not save cache") - } - return -} - -func (ib *imapBackend) loadIMAPCache() error { - if ib.imapCache != nil { - return nil - } - - ib.imapCacheLock.Lock() - defer ib.imapCacheLock.Unlock() - - f, err := os.Open(ib.imapCachePath) - if err != nil { - return err - } - defer f.Close() //nolint:errcheck,gosec - - return json.NewDecoder(f).Decode(&ib.imapCache) -} - -func (ib *imapBackend) saveIMAPCache() error { - if ib.imapCache == nil { - return errors.New("cannot save cache: cache is nil") - } - - ib.imapCacheLock.Lock() - defer ib.imapCacheLock.Unlock() - - f, err := os.Create(ib.imapCachePath) - if err != nil { - return err - } - defer f.Close() //nolint:errcheck,gosec - - return json.NewEncoder(f).Encode(ib.imapCache) -} diff --git a/internal/imap/bridge.go b/internal/imap/bridge.go deleted file mode 100644 index 3183002a..00000000 --- a/internal/imap/bridge.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/users" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -type cacheProvider interface { - GetDBDir() string - GetIMAPCachePath() string -} - -type bridger interface { - GetUser(query string) (bridgeUser, error) - HasError(err error) bool - IsAllMailVisible() bool -} - -type bridgeUser interface { - ID() string - CheckBridgeLogin(password string) error - IsCombinedAddressMode() bool - GetAddressID(address string) (string, error) - GetPrimaryAddress() string - Logout() error - CloseConnection(address string) - GetStore() storeUserProvider - GetClient() pmapi.Client -} - -type bridgeWrap struct { - *bridge.Bridge -} - -// newBridgeWrap wraps bridge struct into local bridgeWrap to implement local -// interface. Problem is that bridge is returning package bridge's User type, -// so every method that returns User has to be overridden to fulfill the interface. -func newBridgeWrap(bridge *bridge.Bridge) *bridgeWrap { - return &bridgeWrap{Bridge: bridge} -} - -func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) { - user, err := b.Bridge.GetUser(query) - if err != nil { - return nil, err - } - return newBridgeUserWrap(user), nil //nolint:typecheck missing methods are inherited -} - -type bridgeUserWrap struct { - *users.User -} - -func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap { - return &bridgeUserWrap{User: bridgeUser} -} - -func (u *bridgeUserWrap) GetStore() storeUserProvider { - store := u.User.GetStore() - if store == nil { - return nil - } - return newStoreUserWrap(store) //nolint:typecheck missing methods are inherited -} diff --git a/internal/imap/id/extension.go b/internal/imap/id/extension.go deleted file mode 100644 index 8b805364..00000000 --- a/internal/imap/id/extension.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package id - -import ( - imapid "github.com/ProtonMail/go-imap-id" - imapserver "github.com/emersion/go-imap/server" -) - -type currentClientSetter interface { - SetClient(name, version string) -} - -// Extension for IMAP server. -type extension struct { - extID imapserver.ConnExtension - clientSetter currentClientSetter -} - -func (ext *extension) Capabilities(conn imapserver.Conn) []string { - return ext.extID.Capabilities(conn) -} - -func (ext *extension) Command(name string) imapserver.HandlerFactory { - newIDHandler := ext.extID.Command(name) - if newIDHandler == nil { - return nil - } - return func() imapserver.Handler { - if hdlrID, ok := newIDHandler().(*imapid.Handler); ok { - return &handler{ - hdlrID: hdlrID, - clientSetter: ext.clientSetter, - } - } - return nil - } -} - -func (ext *extension) NewConn(conn imapserver.Conn) imapserver.Conn { - return ext.extID.NewConn(conn) -} - -type handler struct { - hdlrID *imapid.Handler - clientSetter currentClientSetter -} - -func (hdlr *handler) Parse(fields []interface{}) error { - return hdlr.hdlrID.Parse(fields) -} - -func (hdlr *handler) Handle(conn imapserver.Conn) error { - err := hdlr.hdlrID.Handle(conn) - if err == nil { - id := hdlr.hdlrID.Command.ID - hdlr.clientSetter.SetClient(id[imapid.FieldName], id[imapid.FieldVersion]) - } - return err -} - -// NewExtension returns extension which is adding RFC2871 ID capability, with -// direct interface to set information about email client to backend. -func NewExtension(serverID imapid.ID, clientSetter currentClientSetter) imapserver.Extension { - if conExtID, ok := imapid.NewExtension(serverID).(imapserver.ConnExtension); ok { - return &extension{ - extID: conExtID, - clientSetter: clientSetter, - } - } - return nil -} diff --git a/internal/imap/idle/extension.go b/internal/imap/idle/extension.go deleted file mode 100644 index 273aa761..00000000 --- a/internal/imap/idle/extension.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package idle - -import ( - "bufio" - "errors" - "strings" - - "github.com/emersion/go-imap" - "github.com/emersion/go-imap/server" -) - -const ( - idleCommand = "IDLE" // Capability and Command identificator - doneLine = "DONE" -) - -// Handler for IDLE extension. -type Handler struct{} - -// Command for IDLE handler. -func (h *Handler) Command() *imap.Command { - return &imap.Command{Name: idleCommand} -} - -// Parse for IDLE handler. -func (h *Handler) Parse(fields []interface{}) error { - return nil -} - -// Handle the IDLE request. -func (h *Handler) Handle(conn server.Conn) error { - cont := &imap.ContinuationReq{Info: "idling"} - if err := conn.WriteResp(cont); err != nil { - return err - } - - // Wait for DONE - scanner := bufio.NewScanner(conn) - scanner.Scan() - if err := scanner.Err(); err != nil { - return err - } - - if strings.ToUpper(scanner.Text()) != doneLine { - return errors.New("expected DONE") - } - return nil -} - -type extension struct{} - -func (ext *extension) Capabilities(c server.Conn) []string { - return []string{idleCommand} -} - -func (ext *extension) Command(name string) server.HandlerFactory { - if name != idleCommand { - return nil - } - - return func() server.Handler { - return &Handler{} - } -} - -func NewExtension() server.Extension { - return &extension{} -} diff --git a/internal/imap/imap.go b/internal/imap/imap.go deleted file mode 100644 index a461163c..00000000 --- a/internal/imap/imap.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import "github.com/sirupsen/logrus" - -var log = logrus.WithField("pkg", "imap") //nolint:gochecknoglobals diff --git a/internal/imap/mailbox.go b/internal/imap/mailbox.go deleted file mode 100644 index 4e52f8f8..00000000 --- a/internal/imap/mailbox.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "strings" - "time" - - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/emersion/go-imap" - "github.com/sirupsen/logrus" -) - -type imapMailbox struct { - panicHandler panicHandler - user *imapUser - name string - - log *logrus.Entry - - storeUser storeUserProvider - storeAddress storeAddressProvider - storeMailbox storeMailboxProvider -} - -// newIMAPMailbox returns struct implementing go-imap/mailbox interface. -func newIMAPMailbox(panicHandler panicHandler, user *imapUser, storeMailbox storeMailboxProvider) *imapMailbox { - return &imapMailbox{ - panicHandler: panicHandler, - user: user, - name: storeMailbox.Name(), - - log: log. - WithField("addressID", user.storeAddress.AddressID()). - WithField("userID", user.storeUser.UserID()). - WithField("labelID", storeMailbox.LabelID()), - - storeUser: user.storeUser, - storeAddress: user.storeAddress, - storeMailbox: storeMailbox, - } -} - -// logCommand is helper to log commands requested by IMAP client with their -// params, result, and duration, but without private data. -// It's logged as INFO so it's logged for every user by default. This should -// help devs to find out reasons why clients, mostly Apple Mail, does re-sync. -// FETCH, APPEND, STORE, COPY, MOVE, and EXPUNGE should be using this helper. -func (im *imapMailbox) logCommand(callback func() error, cmd string, params ...interface{}) error { - start := time.Now() - err := callback() - // Not using im.log to not include addressID which is not needed in this case. - log.WithFields(logrus.Fields{ - "userID": im.storeUser.UserID(), - "labelID": im.storeMailbox.LabelID(), - "duration": time.Since(start), - "err": err, - "params": params, - }).Info(cmd) - return err -} - -// Name returns this mailbox name. -func (im *imapMailbox) Name() string { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - return im.name -} - -// Info returns this mailbox info. -func (im *imapMailbox) Info() (*imap.MailboxInfo, error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - info := &imap.MailboxInfo{ - Attributes: im.getFlags(), - Delimiter: im.storeMailbox.GetDelimiter(), - Name: im.name, - } - - return info, nil -} - -func (im *imapMailbox) getFlags() []string { - flags := []string{} - if !im.storeMailbox.IsFolder() || im.storeMailbox.IsSystem() { - flags = append(flags, imap.NoInferiorsAttr) // Subfolders are not supported for System or Label - } - switch im.storeMailbox.LabelID() { - case pmapi.SentLabel: - flags = append(flags, imap.SentAttr) - case pmapi.TrashLabel: - flags = append(flags, imap.TrashAttr) - case pmapi.SpamLabel: - flags = append(flags, imap.JunkAttr) - case pmapi.ArchiveLabel: - flags = append(flags, imap.ArchiveAttr) - case pmapi.AllMailLabel: - flags = append(flags, imap.AllAttr) - case pmapi.DraftLabel: - flags = append(flags, imap.DraftsAttr) - } - - return flags -} - -// Status returns this mailbox status. The fields Name, Flags and -// PermanentFlags in the returned MailboxStatus must be always populated. This -// function does not affect the state of any messages in the mailbox. See RFC -// 3501 section 6.3.10 for a list of items that can be requested. -// -// It always returns the state of DB (which could be different to server status). -// Additionally it checks that all stored numbers are same as in DB and polls events if needed. -func (im *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - l := log.WithField("status-label", im.storeMailbox.LabelID()) - l.Data["user"] = im.storeUser.UserID() - l.Data["address"] = im.storeAddress.AddressID() - status := imap.NewMailboxStatus(im.name, items) - status.UidValidity = im.storeMailbox.UIDValidity() - status.Flags = []string{ - imap.SeenFlag, strings.ToUpper(imap.SeenFlag), - imap.FlaggedFlag, strings.ToUpper(imap.FlaggedFlag), - imap.DeletedFlag, strings.ToUpper(imap.DeletedFlag), - imap.DraftFlag, strings.ToUpper(imap.DraftFlag), - message.AppleMailJunkFlag, - message.ThunderbirdJunkFlag, - message.ThunderbirdNonJunkFlag, - } - status.PermanentFlags = append([]string{}, status.Flags...) - - dbTotal, dbUnread, dbUnreadSeqNum, err := im.storeMailbox.GetCounts() - l.WithFields(logrus.Fields{ - "total": dbTotal, - "unread": dbUnread, - "unreadSeqNum": dbUnreadSeqNum, - "err": err, - }).Debug("DB counts") - if err == nil { - status.Messages = uint32(dbTotal) - status.Unseen = uint32(dbUnread) - status.UnseenSeqNum = uint32(dbUnreadSeqNum) - } - - if status.UidNext, err = im.storeMailbox.GetNextUID(); err != nil { - return nil, err - } - - return status, nil -} - -// SetSubscribed adds or removes the mailbox to the server's set of "active" -// or "subscribed" mailboxes. -func (im *imapMailbox) SetSubscribed(subscribed bool) error { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - label := im.storeMailbox.LabelID() - if subscribed && !im.user.isSubscribed(label) { - im.user.removeFromCache(SubscriptionException, label) - } - if !subscribed && im.user.isSubscribed(label) { - im.user.addToCache(SubscriptionException, label) - } - return nil -} - -// Check requests a checkpoint of the currently selected mailbox. A checkpoint -// refers to any implementation-dependent housekeeping associated with the -// mailbox (e.g., resolving the server's in-memory state of the mailbox with -// the state on its disk). A checkpoint MAY take a non-instantaneous amount of -// real time to complete. If a server implementation has no such housekeeping -// considerations, CHECK is equivalent to NOOP. -func (im *imapMailbox) Check() error { - return nil -} - -// Expunge permanently removes all messages that have the \Deleted flag set -// from the currently selected mailbox. -func (im *imapMailbox) Expunge() error { - // See comment of appendExpungeLock. - if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel { - im.user.appendExpungeLock.Lock() - defer im.user.appendExpungeLock.Unlock() - } - - return im.logCommand(im.expunge, "EXPUNGE") -} - -func (im *imapMailbox) expunge() error { - im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage) - defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage) - - return im.storeMailbox.RemoveDeleted(nil) -} - -// UIDExpunge permanently removes messages that have the \Deleted flag set -// and UID passed from SeqSet from the currently selected mailbox. -func (im *imapMailbox) UIDExpunge(seqSet *imap.SeqSet) error { - return im.logCommand(func() error { - return im.uidExpunge(seqSet) - }, "UID EXPUNGE", seqSet) -} - -func (im *imapMailbox) uidExpunge(seqSet *imap.SeqSet) error { - // See comment of appendExpungeLock. - if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel { - im.user.appendExpungeLock.Lock() - defer im.user.appendExpungeLock.Unlock() - } - - im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage) - defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage) - - messageIDs, err := im.apiIDsFromSeqSet(true, seqSet) - if err != nil || len(messageIDs) == 0 { - return err - } - return im.storeMailbox.RemoveDeleted(messageIDs) -} - -func (im *imapMailbox) ListQuotas() ([]string, error) { - return []string{""}, nil -} diff --git a/internal/imap/mailbox_append.go b/internal/imap/mailbox_append.go deleted file mode 100644 index 09fc0dc8..00000000 --- a/internal/imap/mailbox_append.go +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "bufio" - "bytes" - "fmt" - "io" - "net/mail" - "strings" - "time" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/imap/uidplus" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/emersion/go-imap" - "github.com/emersion/go-message/textproto" - "github.com/pkg/errors" -) - -// CreateMessage appends a new message to this mailbox. The \Recent flag will -// be added regardless of whether flags is empty or not. If date is nil, the -// current time will be used. -// -// If the Backend implements Updater, it must notify the client immediately -// via a mailbox update. -func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { - return im.logCommand(func() error { - return im.createMessage(flags, date, body) - }, "APPEND", flags, date) -} - -func (im *imapMailbox) createMessage(imapFlags []string, date time.Time, r imap.Literal) error { //nolint:funlen - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - // NOTE: Is this lock meant to be here? - im.user.appendExpungeLock.Lock() - defer im.user.appendExpungeLock.Unlock() - - body, err := io.ReadAll(r) - if err != nil { - return err - } - - addr := im.storeAddress.APIAddress() - if addr == nil { - return errors.New("no available address for encryption") - } - - kr, err := im.user.client().KeyRingForAddressID(addr.ID) - if err != nil { - return err - } - - if im.storeMailbox.LabelID() == pmapi.DraftLabel { - return im.createDraftMessage(kr, addr.Email, body) - } - - if im.storeMailbox.LabelID() == pmapi.SentLabel { - m, _, _, _, err := message.Parse(bytes.NewReader(body)) - if err != nil { - return err - } - - if m.Sender == nil { - m.Sender = &mail.Address{Address: addr.Email} - } - - if user, err := im.user.backend.bridge.GetUser(pmapi.SanitizeEmail(m.Sender.Address)); err == nil && user.ID() == im.storeUser.UserID() { - logEntry := im.log.WithField("sender", m.Sender).WithField("extID", m.Header.Get("Message-Id")).WithField("date", date) - - if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) { - logEntry.Info("Ignoring APPEND of duplicate to Sent folder") - return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID}) - } - - logEntry.Info("No matching UID, continuing APPEND to Sent") - } - } - - hdr, err := textproto.ReadHeader(bufio.NewReader(bytes.NewReader(body))) - if err != nil { - return err - } - - // Avoid appending a message which is already on the server. Apply the new label instead. - // This always happens with Outlook because it uses APPEND instead of COPY. - internalID := hdr.Get("X-Pm-Internal-Id") - - // In case there is a mail client which corrupts headers, try "References" too. - if internalID == "" { - if references := strings.Fields(hdr.Get("References")); len(references) > 0 { - if match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(references[len(references)-1]); len(match) == 2 { - internalID = match[1] - } - } - } - - if internalID != "" { - if msg, err := im.storeMailbox.GetMessage(internalID); err == nil { - if im.user.user.IsCombinedAddressMode() || im.storeAddress.AddressID() == msg.Message().AddressID { - return im.labelExistingMessage(msg) - } - } - } - return im.importMessage(kr, hdr, body, imapFlags, date) -} - -func (im *imapMailbox) createDraftMessage(kr *crypto.KeyRing, email string, body []byte) error { - im.log.Info("Creating draft message") - - m, _, _, readers, err := message.Parse(bytes.NewReader(body)) - if err != nil { - return err - } - - if m.Sender == nil { - m.Sender = &mail.Address{} - } - - m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, email) - - draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "") - if err != nil { - return errors.Wrap(err, "failed to create draft") - } - - return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{draft.ID})) -} - -func findMailboxForAddress(address storeAddressProvider, labelID string) (storeMailboxProvider, error) { - for _, mailBox := range address.ListMailboxes() { - if mailBox.LabelID() == labelID { - return mailBox, nil - } - } - return nil, fmt.Errorf("could not find %v label in mailbox for user %v", labelID, - address.AddressString()) -} - -func (im *imapMailbox) labelExistingMessage(msg storeMessageProvider) error { //nolint:funlen - im.log.Info("Labelling existing message") - - // IMAP clients can move message to local folder (setting \Deleted flag) - // and then move it back (IMAP client does not remember the message, - // so instead removing the flag it imports duplicate message). - // Regular IMAP server would keep the message twice and later EXPUNGE would - // not delete the message (EXPUNGE would delete the original message and - // the new duplicate one would stay). API detects duplicates; therefore - // we need to remove \Deleted flag if IMAP client re-imports. - if msg.IsMarkedDeleted() { - if err := im.storeMailbox.MarkMessagesUndeleted([]string{msg.ID()}); err != nil { - log.WithError(err).Error("Failed to undelete re-imported message") - } - } - - // Outlook Uses APPEND instead of COPY. There is no need to copy to All Mail because messages are already there. - // If the message is copied from Spam or Trash, it must be moved otherwise we will have data loss. - // If the message is moved from any folder, the moment when expunge happens on source we will move message trash unless we move it to archive. - // If the message is already in Archive we should not call API at all. - // Otherwise the message is already in All mail, Return OK. - storeMBox := im.storeMailbox - if pmapi.AllMailLabel == storeMBox.LabelID() { - if msg.Message().HasLabelID(pmapi.ArchiveLabel) { - return uidplus.AppendResponse(storeMBox.UIDValidity(), storeMBox.GetUIDList([]string{msg.ID()})) - } - var err error - storeMBox, err = findMailboxForAddress(im.storeAddress, pmapi.ArchiveLabel) - if err != nil { - return err - } - } - - if err := storeMBox.LabelMessages([]string{msg.ID()}); err != nil { - return err - } - - return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{msg.ID()})) -} - -func (im *imapMailbox) importMessage(kr *crypto.KeyRing, hdr textproto.Header, body []byte, imapFlags []string, date time.Time) error { //nolint:funlen - im.log.WithField("size", len(body)).Info("Importing external message") - - var ( - seen bool - flags int64 - labelIDs []string - time int64 - ) - - if hdr.Get("received") == "" { - flags = pmapi.FlagSent - } else { - flags = pmapi.FlagReceived - } - - for _, flag := range imapFlags { - switch flag { - case imap.DraftFlag: - flags &= ^pmapi.FlagSent - flags &= ^pmapi.FlagReceived - - case imap.SeenFlag: - seen = true - - case imap.FlaggedFlag: - labelIDs = append(labelIDs, pmapi.StarredLabel) - - case imap.AnsweredFlag: - flags |= pmapi.FlagReplied - } - } - - if !date.IsZero() { - time = date.Unix() - } - - enc, err := message.EncryptRFC822(kr, bytes.NewReader(body)) - if err != nil { - return err - } - - targetMailbox := im.storeMailbox - if targetMailbox.LabelID() == pmapi.AllMailLabel { - // Importing mail in directly into All Mail is not allowed. Instead we redirect the import to Archive - // The mail will automatically appear in All mail. The appends response still reports that the mail was - // successfully APPEND to All Mail. - targetMailbox, err = findMailboxForAddress(im.storeAddress, pmapi.ArchiveLabel) - if err != nil { - return err - } - } - - messageID, err := targetMailbox.ImportMessage(enc, seen, labelIDs, flags, time) - if err != nil { - log.WithField("enc.size", len(enc)).Error("Import failed") - return err - } - - msg, err := targetMailbox.GetMessage(messageID) - if err != nil { - return err - } - - if msg.IsMarkedDeleted() { - if err := targetMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil { - log.WithError(err).Error("Failed to undelete re-imported message") - } - } - - return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID})) -} diff --git a/internal/imap/mailbox_fetch.go b/internal/imap/mailbox_fetch.go deleted file mode 100644 index f82aecb2..00000000 --- a/internal/imap/mailbox_fetch.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "bytes" - - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/emersion/go-imap" - "github.com/pkg/errors" -) - -func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem) (msg *imap.Message, err error) { - msglog := im.log.WithField("msgID", storeMessage.ID()) - msglog.Trace("Getting message") - - seqNum, err := storeMessage.SequenceNumber() - if err != nil { - return - } - - m := storeMessage.Message() - - msg = imap.NewMessage(seqNum, items) - for _, item := range items { - switch item { - case imap.FetchEnvelope: - // No need to retrieve full header here. API header - // contains enough information to build the envelope. - msg.Envelope = message.GetEnvelope(m, storeMessage.GetMIMEHeaderFast()) - case imap.FetchBody, imap.FetchBodyStructure: - structure, err := im.getBodyStructure(storeMessage) - if err != nil { - return nil, err - } - if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil { - return nil, err - } - case imap.FetchFlags: - msg.Flags = message.GetFlags(m) - if storeMessage.IsMarkedDeleted() { - msg.Flags = append(msg.Flags, imap.DeletedFlag) - } - case imap.FetchInternalDate: - // Apple Mail crashes fetching messages with date older than 1970. - // There is no point having message older than RFC itself, it's not possible. - msg.InternalDate = message.SanitizeMessageDate(m.Time) - case imap.FetchRFC822Size: - size, err := storeMessage.GetRFC822Size() - if err != nil { - return nil, err - } - - msg.Size = size - case imap.FetchUid: - if msg.Uid, err = storeMessage.UID(); err != nil { - return nil, err - } - case imap.FetchAll, imap.FetchFast, imap.FetchFull, imap.FetchRFC822, imap.FetchRFC822Header, imap.FetchRFC822Text: - fallthrough // this is list of defined items by go-imap, but items can be also sections generated from requests - default: - if err = im.getLiteralForSection(item, msg, storeMessage); err != nil { - return - } - } - } - - return msg, err -} - -func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider) error { - section, err := imap.ParseBodySectionName(itemSection) - if err != nil { - log.WithError(err).Warn("Failed to parse body section name; part will be skipped") - return nil //nolint:nilerr ignore error - } - - var literal imap.Literal - if literal, err = im.getMessageBodySection(storeMessage, section); err != nil { - return err - } - - msg.Body[section] = literal - return nil -} - -// getBodyStructure returns the cached body structure or it will build the message, -// save the structure in DB and then returns the structure after build. -// -// Apple Mail requests body structure for all messages irregularly. We cache -// bodystructure in local database in order to not re-download all messages -// from server. -func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *message.BodyStructure, err error) { - bs, err = storeMessage.GetBodyStructure() - if err != nil { - im.log.WithError(err).Debug("Fail to retrieve bodystructure from database") - } - if bs == nil { - // We are sure the body structure is not a problem right now. - // Clients might do first fetch body structure so we couldn't - // be sure if seeing 1st or 2nd sync is all right or not. - // Therefore, it's better to exclude first body structure fetch - // from the counting and see build count as real message build. - if bs, _, err = im.getBodyAndStructure(storeMessage); err != nil { - return - } - } - return -} - -func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) (*message.BodyStructure, *bytes.Reader, error) { - rfc822, err := storeMessage.GetRFC822() - if err != nil { - return nil, nil, err - } - - structure, err := storeMessage.GetBodyStructure() - if err != nil { - return nil, nil, err - } - - return structure, bytes.NewReader(rfc822), nil -} - -// This will download message (or read from cache) and pick up the section, -// extract data (header,body, both) and trim the output if needed. -// -// In order to speed up (avoid download and decryptions) we -// cache the header. If a mail header was requested and DB -// contains full header (it means it was already built once) -// the DB header can be used without downloading and decrypting. -// Otherwise header is incomplete and clients would have issues -// e.g. AppleMail expects `text/plain` in HTML mails. -// -// For all other cases it is necessary to download and decrypt the message -// and drop the header which was obtained from cache. The header will -// will be stored in DB once successfully built. Check `getBodyAndStructure`. -func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider, section *imap.BodySectionName) (imap.Literal, error) { - var header []byte - var response []byte - - im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body") - - isMainHeaderRequested := len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier - if isMainHeaderRequested && storeMessage.IsFullHeaderCached() { - var err error - if header, err = storeMessage.GetHeader(); err != nil { - return nil, err - } - } else { - structure, bodyReader, err := im.getBodyAndStructure(storeMessage) - if err != nil { - return nil, err - } - - switch { - case section.Specifier == imap.EntireSpecifier && len(section.Path) == 0: - // An empty section specification refers to the entire message, including the header. - response, err = structure.GetSection(bodyReader, section.Path) - case section.Specifier == imap.TextSpecifier || (section.Specifier == imap.EntireSpecifier && len(section.Path) != 0): - // The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header. - // Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header. - response, err = structure.GetSectionContent(bodyReader, section.Path) - case section.Specifier == imap.MIMESpecifier: // The MIME part specifier refers to the [MIME-IMB] header for this part. - fallthrough - case section.Specifier == imap.HeaderSpecifier: - header, err = structure.GetSectionHeaderBytes(section.Path) - default: - err = errors.New("Unknown specifier " + string(section.Specifier)) - } - - if err != nil { - return nil, err - } - } - - if header != nil { - response = filterHeader(header, section) - } - - // Trim any output if requested. - return bytes.NewBuffer(section.ExtractPartial(response)), nil -} diff --git a/internal/imap/mailbox_fetch_test.go b/internal/imap/mailbox_fetch_test.go deleted file mode 100644 index e6be9282..00000000 --- a/internal/imap/mailbox_fetch_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFilterHeader(t *testing.T) { - const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n\r\n" - - assert.Equal(t, "To: somebody\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool { - return strings.EqualFold(field, "To") - }))) - - assert.Equal(t, "From: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool { - return strings.EqualFold(field, "From") - }))) - - assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool { - return strings.EqualFold(field, "To") || strings.EqualFold(field, "From") - }))) - - assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool { - return strings.EqualFold(field, "Subject") - }))) -} - -// TestFilterHeaderNoNewline tests that we don't include a trailing newline when filtering -// if the original header also lacks one (which it can legally do if there is no body). -func TestFilterHeaderNoNewline(t *testing.T) { - const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n" - - assert.Equal(t, "To: somebody\r\n", string(filterHeaderLines([]byte(header), func(field string) bool { - return strings.EqualFold(field, "To") - }))) - - assert.Equal(t, "From: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool { - return strings.EqualFold(field, "From") - }))) - - assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool { - return strings.EqualFold(field, "To") || strings.EqualFold(field, "From") - }))) - - assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n", string(filterHeaderLines([]byte(header), func(field string) bool { - return strings.EqualFold(field, "Subject") - }))) -} diff --git a/internal/imap/mailbox_header.go b/internal/imap/mailbox_header.go deleted file mode 100644 index f4461684..00000000 --- a/internal/imap/mailbox_header.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "bytes" - "strings" - - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/emersion/go-imap" -) - -func filterHeader(header []byte, section *imap.BodySectionName) []byte { - // Empty section.Fields means BODY[HEADER] was requested so we should return the full header. - if len(section.Fields) == 0 { - return header - } - - fieldMap := make(map[string]struct{}) - - for _, field := range section.Fields { - fieldMap[strings.ToLower(field)] = struct{}{} - } - - return filterHeaderLines(header, func(field string) bool { - _, ok := fieldMap[strings.ToLower(field)] - - if section.NotFields { - ok = !ok - } - - return ok - }) -} - -func filterHeaderLines(header []byte, wantField func(string) bool) []byte { - var res []byte - - for _, line := range message.HeaderLines(header) { - if len(bytes.TrimSpace(line)) == 0 { - res = append(res, line...) - } else { - split := bytes.SplitN(line, []byte(": "), 2) - - if len(split) != 2 { - continue - } - - if wantField(string(bytes.ToLower(split[0]))) { - res = append(res, line...) - } - } - } - - return res -} diff --git a/internal/imap/mailbox_messages.go b/internal/imap/mailbox_messages.go deleted file mode 100644 index c3ce6359..00000000 --- a/internal/imap/mailbox_messages.go +++ /dev/null @@ -1,676 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "fmt" - "net/mail" - "strings" - "sync" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/imap/uidplus" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/parallel" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/emersion/go-imap" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// UpdateMessagesFlags alters flags for the specified message(s). -// -// If the Backend implements Updater, it must notify the client immediately -// via a message update. -func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error { - return im.logCommand(func() error { - return im.updateMessagesFlags(uid, seqSet, operation, flags) - }, "STORE", uid, seqSet, operation, flags) -} - -func (im *imapMailbox) updateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error { - log.WithFields(logrus.Fields{ - "flags": flags, - "operation": operation, - }).Debug("Updating message flags") - - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationUpdateMessage) - defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationUpdateMessage) - - messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet) - if err != nil || len(messageIDs) == 0 { - return err - } - - if operation == imap.SetFlags { - return im.setFlags(messageIDs, flags) - } - return im.addOrRemoveFlags(operation, messageIDs, flags) -} - -// setFlags is used for FLAGS command (not +FLAGS or -FLAGS), which means -// to set flags passed as an argument and unset the rest. For example, -// if message is not read, is flagged and is not deleted, call FLAGS \Seen -// should flag message as read, unflagged and keep undeleted. -func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint:funlen - seen := false - flagged := false - deleted := false - spam := false - - for _, f := range flags { - switch f { - case imap.SeenFlag: - seen = true - case imap.FlaggedFlag: - flagged = true - case imap.DeletedFlag: - deleted = true - case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag: - spam = true - } - } - - if seen { - if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil { - return err - } - } else { - if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil { - return err - } - } - - if flagged { - if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil { - return err - } - } else { - if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil { - return err - } - } - - if deleted { - if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil { - return err - } - } else { - if err := im.storeMailbox.MarkMessagesUndeleted(messageIDs); err != nil { - return err - } - } - - // Spam should not be taken into action here as Outlook is using FLAGS - // without preserving junk flag. Probably it's because junk is not standard - // in the rfc3501 and thus Outlook expects calling FLAGS \Seen will not - // change the state of junk or other non-standard flags. - // Still, its safe to label as spam once any client sends the request. - if spam { - spamMailbox, err := im.storeAddress.GetMailbox("Spam") - if err != nil { - return err - } - if err := spamMailbox.LabelMessages(messageIDs); err != nil { - return err - } - } - - return nil -} - -func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flags []string) error { //nolint:funlen - for _, f := range flags { - // Adding flag 'nojunk' is equivalent to removing flag 'junk' - if (operation == imap.AddFlags) && (f == "nojunk") { - operation = imap.RemoveFlags - f = "junk" - } - - switch f { - case imap.SeenFlag: - switch operation { //nolint:exhaustive // imap.SetFlags is processed by im.setFlags - case imap.AddFlags: - if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil { - return err - } - case imap.RemoveFlags: - if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil { - return err - } - } - case imap.FlaggedFlag: - switch operation { //nolint:exhaustive // imap.SetFlag is processed by im.setFlags - case imap.AddFlags: - if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil { - return err - } - case imap.RemoveFlags: - if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil { - return err - } - } - case imap.DeletedFlag: - switch operation { //nolint:exhaustive // imap.SetFlag is processed by im.setFlags - case imap.AddFlags: - if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil { - return err - } - case imap.RemoveFlags: - if err := im.storeMailbox.MarkMessagesUndeleted(messageIDs); err != nil { - return err - } - } - case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag: - // Not supported. - case strings.ToLower(message.AppleMailJunkFlag), strings.ToLower(message.ThunderbirdJunkFlag): - spamMailbox, err := im.storeAddress.GetMailbox("Spam") - if err != nil { - return err - } - // Handle custom junk flags for Apple Mail and Thunderbird. - switch operation { //nolint:exhaustive // imap.SetFlag is processed by im.setFlags - // No label removal is necessary because Spam and Inbox are both exclusive labels so the backend - // will automatically take care of label removal. - case imap.AddFlags: - if err := spamMailbox.LabelMessages(messageIDs); err != nil { - return err - } - case imap.RemoveFlags: - // During spam flag removal only messages which - // are in Spam folder should be moved to Inbox. - // For other messages it is NOOP. - messagesInSpam := []string{} - for _, mID := range messageIDs { - if uid := spamMailbox.GetUIDList([]string{mID}); len(*uid) != 0 { - messagesInSpam = append(messagesInSpam, mID) - } - } - if len(messagesInSpam) != 0 { - inboxMailbox, err := im.storeAddress.GetMailbox("INBOX") - if err != nil { - return err - } - if err := inboxMailbox.LabelMessages(messagesInSpam); err != nil { - return err - } - } - } - } - } - - return nil -} - -// CopyMessages copies the specified message(s) to the end of the specified -// destination mailbox. The flags and internal date of the message(s) SHOULD -// be preserved, and the Recent flag SHOULD be set, in the copy. -func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error { - return im.logCommand(func() error { - return im.copyMessages(uid, seqSet, targetLabel) - }, "COPY", uid, seqSet, targetLabel) -} - -func (im *imapMailbox) copyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - return im.labelMessages(uid, seqSet, targetLabel, false) -} - -// MoveMessages adds dest's label and removes this mailbox' label from each message. -// -// This should not be used until MOVE extension has option to send UIDPLUS -// responses. -func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error { - return im.logCommand(func() error { - return im.moveMessages(uid, seqSet, targetLabel) - }, "MOVE", uid, seqSet, targetLabel) -} - -func (im *imapMailbox) moveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - // Moving from All Mail is not allowed. - if im.storeMailbox.LabelID() == pmapi.AllMailLabel { - return errors.New("move from All Mail is not allowed") - } - return im.labelMessages(uid, seqSet, targetLabel, true) -} - -func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel string, move bool) error { //nolint:funlen - messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet) - if err != nil || len(messageIDs) == 0 { - return err - } - - // It is needed to get UID list before LabelingMessages because - // messages can be removed from source during labeling (e.g. folder1 -> folder2). - sourceSeqSet := im.storeMailbox.GetUIDList(messageIDs) - - targetStoreMailbox, err := im.storeAddress.GetMailbox(targetLabel) - if err != nil { - return err - } - - // Moving or copying from Inbox to Sent or from Sent to Inbox is no-op. - // Inbox and Sent is the same mailbox and message is showen in one or - // the other based on message flags. - // COPY operation has to be forbidden otherwise move by COPY+EXPUNGE - // would lead to message found only in All Mail, because COPY is no-op - // and EXPUNGE is translated as unlabel from the source. - // MOVE operation could be allowed, just it will do no change. It's better - // to refuse it as well so client is kept in proper state and no sync - // is needed. - isInboxOrSent := func(labelID string) bool { - return labelID == pmapi.InboxLabel || labelID == pmapi.SentLabel - } - if isInboxOrSent(im.storeMailbox.LabelID()) && isInboxOrSent(targetStoreMailbox.LabelID()) { - if im.storeMailbox.LabelID() == pmapi.InboxLabel { - return errors.New("move from Inbox to Sent is not allowed") - } - return errors.New("move from Sent to Inbox is not allowed") - } - - deletedIDs := []string{} - allDeletedIDs, err := im.storeMailbox.GetDeletedAPIIDs() - if err != nil { - log.WithError(err).Warn("Problem to get deleted API IDs") - } else { - for _, messageID := range messageIDs { - for _, deletedID := range allDeletedIDs { - if messageID == deletedID { - deletedIDs = append(deletedIDs, deletedID) - } - } - } - } - - // Label messages first to not lose them. If message is only in trash and we unlabel - // it, it will be removed completely and we cannot label it back. - if err := targetStoreMailbox.LabelMessages(messageIDs); err != nil { - return err - } - // Folder cannot be unlabeled. Every message has to belong to exactly one folder. - // In case of labeling message to folder, the original one is implicitly unlabeled. - // Therefore, we have to unlabel explicitly only if the source mailbox is label. - if im.storeMailbox.IsLabel() && move { - if err := im.storeMailbox.UnlabelMessages(messageIDs); err != nil { - return err - } - } - - // Preserve \Deleted flag at target location. - if len(deletedIDs) > 0 { - if err := targetStoreMailbox.MarkMessagesDeleted(deletedIDs); err != nil { - log.WithError(err).Warn("Problem to preserve deleted flag for copied messages") - } - } - - targetSeqSet := targetStoreMailbox.GetUIDList(messageIDs) - return uidplus.CopyResponse(targetStoreMailbox.UIDValidity(), sourceSeqSet, targetSeqSet) -} - -// SearchMessages searches messages. The returned list must contain UIDs if -// uid is set to true, or sequence numbers otherwise. -func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { - err = im.logCommand(func() error { - var searchError error - ids, searchError = im.searchMessages(isUID, criteria) - return searchError - }, "SEARCH", isUID, criteria.Format()) - return ids, err -} - -func (im *imapMailbox) searchMessages(isUID bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { //nolint:gocyclo,funlen - // Called from go-imap in goroutines - we need to handle panics for each function. - defer im.panicHandler.HandlePanic() - - if criteria.Not != nil || criteria.Or != nil { - return nil, errors.New("unsupported search query") - } - - if criteria.Body != nil || criteria.Text != nil { - log.Warn("Body and Text criteria not applied") - } - - var apiIDs []string - if criteria.SeqNum != nil { - apiIDs, err = im.apiIDsFromSeqSet(false, criteria.SeqNum) - } else { - apiIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(1, 0) - } - if err != nil { - return nil, err - } - - if criteria.Uid != nil { - apiIDsByUID, err := im.apiIDsFromSeqSet(true, criteria.Uid) - if err != nil { - return nil, err - } - apiIDs = arrayIntersection(apiIDs, apiIDsByUID) - } - - for _, apiID := range apiIDs { - // Get message. - storeMessage, err := im.storeMailbox.GetMessage(apiID) - if err != nil { - log.Warnf("search messages: cannot get message %q from db: %v", apiID, err) - continue - } - m := storeMessage.Message() - - // Filter by time. - if !criteria.Before.IsZero() { - if truncated := criteria.Before.Truncate(24 * time.Hour); m.Time > truncated.Unix() { - continue - } - } - if !criteria.Since.IsZero() { - if truncated := criteria.Since.Truncate(24 * time.Hour); m.Time < truncated.Unix() { - continue - } - } - - // In order to speed up search it is not needed to always - // retrieve the fully cached header. - header := storeMessage.GetMIMEHeaderFast() - - if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() { - t, err := mail.Header(header).Date() - if err != nil || t.IsZero() { - t = time.Unix(m.Time, 0) - } - if !criteria.SentBefore.IsZero() { - if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() { - continue - } - } - if !criteria.SentSince.IsZero() { - if truncated := criteria.SentSince.Truncate(24 * time.Hour); t.Unix() < truncated.Unix() { - continue - } - } - } - - // Filter by headers. - headerMatch := true - for criteriaKey, criteriaValues := range criteria.Header { - for _, criteriaValue := range criteriaValues { - if criteriaValue == "" { - continue - } - switch criteriaKey { - case "Subject": - headerMatch = strings.Contains(strings.ToLower(m.Subject), strings.ToLower(criteriaValue)) - case "From": - headerMatch = addressMatch([]*mail.Address{m.Sender}, criteriaValue) - case "To": - headerMatch = addressMatch(m.ToList, criteriaValue) - case "Cc": - headerMatch = addressMatch(m.CCList, criteriaValue) - case "Bcc": - headerMatch = addressMatch(m.BCCList, criteriaValue) - default: - if messageValue := header.Get(criteriaKey); messageValue == "" { - headerMatch = false // Field is not in header. - } else if !strings.Contains(strings.ToLower(messageValue), strings.ToLower(criteriaValue)) { - headerMatch = false // Field is in header but value not matched (case insensitive). - } - } - if !headerMatch { - break - } - } - if !headerMatch { - break - } - } - if !headerMatch { - continue - } - - // Filter by flags. - messageFlagsMap := make(map[string]bool) - if isStringInList(m.LabelIDs, pmapi.StarredLabel) { - messageFlagsMap[imap.FlaggedFlag] = true - } - if !m.Unread { - messageFlagsMap[imap.SeenFlag] = true - } - if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) { - messageFlagsMap[imap.AnsweredFlag] = true - } - if m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived) { - messageFlagsMap[imap.DraftFlag] = true - } - if !m.Has(pmapi.FlagOpened) { - messageFlagsMap[imap.RecentFlag] = true - } - if storeMessage.IsMarkedDeleted() { - messageFlagsMap[imap.DeletedFlag] = true - } - - flagMatch := true - for _, flag := range criteria.WithFlags { - if !messageFlagsMap[flag] { - flagMatch = false - break - } - } - for _, flag := range criteria.WithoutFlags { - if messageFlagsMap[flag] { - flagMatch = false - break - } - } - if !flagMatch { - continue - } - - // Filter by size (only if size was already calculated). - size, err := storeMessage.GetRFC822Size() - if err != nil { - return nil, err - } - - if size > 0 { - if criteria.Larger != 0 && int64(size) <= int64(criteria.Larger) { - continue - } - if criteria.Smaller != 0 && int64(size) >= int64(criteria.Smaller) { - continue - } - } - - // Add the ID to response. - var id uint32 - if isUID { - id, err = storeMessage.UID() - if err != nil { - return nil, err - } - } else { - id, err = storeMessage.SequenceNumber() - if err != nil { - return nil, err - } - } - ids = append(ids, id) - } - - return ids, nil -} - -// ListMessages returns a list of messages. seqset must be interpreted as UIDs -// if uid is set to true and as message sequence numbers otherwise. See RFC -// 3501 section 6.4.5 for a list of items that can be requested. -// -// Messages must be sent to msgResponse. When the function returns, msgResponse must be closed. -func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) error { - return im.logCommand(func() error { - return im.listMessages(isUID, seqSet, items, msgResponse) - }, "FETCH", isUID, seqSet, items) -} - -func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) (err error) { //nolint:funlen - defer func() { - close(msgResponse) - if err != nil { - log.Errorf("cannot list messages (%v, %v, %v): %v", isUID, seqSet, items, err) - } - // Called from go-imap in goroutines - we need to handle panics for each function. - im.panicHandler.HandlePanic() - }() - - if !isUID { - // EXPUNGE cannot be sent during listing and can come only from - // the event loop, so we prevent any server side update to avoid - // the problem. - im.user.backend.updates.forbidExpunge(im.storeMailbox.LabelID()) - defer im.user.backend.updates.allowExpunge(im.storeMailbox.LabelID()) - } - - var markAsReadIDs []string - markAsReadMutex := &sync.Mutex{} - - l := log.WithField("cmd", "ListMessages") - - apiIDs, err := im.apiIDsFromSeqSet(isUID, seqSet) - if err != nil { - err = fmt.Errorf("list messages seq: %v", err) - l.WithField("seq", seqSet).Error(err) - return err - } - - input := make([]interface{}, len(apiIDs)) - for i, apiID := range apiIDs { - input[i] = apiID - } - - processCallback := func(value interface{}) (interface{}, error) { - apiID := value.(string) //nolint:forcetypeassert // we want to panic here - - storeMessage, err := im.storeMailbox.GetMessage(apiID) - if err != nil { - err = fmt.Errorf("list message from db: %v", err) - l.WithField("apiID", apiID).Error(err) - return nil, err - } - - msg, err := im.getMessage(storeMessage, items) - if err != nil { - err = fmt.Errorf("list message build: %v", err) - l.WithField("metaID", storeMessage.ID()).Error(err) - return nil, err - } - - if storeMessage.Message().Unread { - for section := range msg.Body { - // Peek means get messages without marking them as read. - // If client does not only ask for peek, we have to mark them as read. - if !section.Peek { - markAsReadMutex.Lock() - markAsReadIDs = append(markAsReadIDs, storeMessage.ID()) - markAsReadMutex.Unlock() - msg.Flags = append(msg.Flags, imap.SeenFlag) - break - } - } - } - - return msg, nil - } - - collectCallback := func(idx int, value interface{}) error { - msg := value.(*imap.Message) //nolint:forcetypeassert // we want to panic here - msgResponse <- msg - return nil - } - - err = parallel.RunParallel(im.user.backend.listWorkers, input, processCallback, collectCallback) - if err != nil { - return err - } - - if len(markAsReadIDs) > 0 { - if err := im.storeMailbox.MarkMessagesRead(markAsReadIDs); err != nil { - l.Warnf("Cannot mark messages as read: %v", err) - } - } - return nil -} - -// apiIDsFromSeqSet takes an IMAP sequence set (which can contain either -// sequence numbers or UIDs) and returns all known API IDs in this range. -func (im *imapMailbox) apiIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]string, error) { - apiIDs := []string{} - for _, seq := range seqSet.Set { - var newAPIIDs []string - var err error - if uid { - newAPIIDs, err = im.storeMailbox.GetAPIIDsFromUIDRange(seq.Start, seq.Stop) - } else { - newAPIIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(seq.Start, seq.Stop) - } - if err != nil { - return []string{}, err - } - apiIDs = append(apiIDs, newAPIIDs...) - } - if len(apiIDs) == 0 { - log.Debugf("Requested empty message list: %v %v", uid, seqSet) - } - return apiIDs, nil -} - -func arrayIntersection(a, b []string) (c []string) { - m := make(map[string]bool) - for _, item := range a { - m[item] = true - } - for _, item := range b { - if _, ok := m[item]; ok { - c = append(c, item) - } - } - return -} - -func isStringInList(list []string, s string) bool { - for _, v := range list { - if v == s { - return true - } - } - return false -} - -func addressMatch(addresses []*mail.Address, criteria string) bool { - for _, addr := range addresses { - if strings.Contains(strings.ToLower(addr.String()), strings.ToLower(criteria)) { - return true - } - } - return false -} diff --git a/internal/imap/mailbox_root.go b/internal/imap/mailbox_root.go deleted file mode 100644 index c840ebd9..00000000 --- a/internal/imap/mailbox_root.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "errors" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/store" - - imap "github.com/emersion/go-imap" -) - -// The mailbox containing all custom folders or labels. -// The purpose of this mailbox is to see "Folders" and "Labels" -// at the root of the mailbox tree, e.g.: -// -// Folders << this -// Folders/Family -// -// Labels << this -// Labels/Security -// -// This mailbox cannot be modified or read in any way. -type imapRootMailbox struct { - isFolder bool -} - -func newFoldersRootMailbox() *imapRootMailbox { - return &imapRootMailbox{isFolder: true} -} - -func newLabelsRootMailbox() *imapRootMailbox { - return &imapRootMailbox{isFolder: false} -} - -func (m *imapRootMailbox) Name() string { - if m.isFolder { - return store.UserFoldersMailboxName - } - return store.UserLabelsMailboxName -} - -func (m *imapRootMailbox) Info() (info *imap.MailboxInfo, err error) { - info = &imap.MailboxInfo{ - Attributes: []string{imap.NoSelectAttr}, - Delimiter: store.PathDelimiter, - } - - if m.isFolder { - info.Name = store.UserFoldersMailboxName - } else { - info.Name = store.UserLabelsMailboxName - } - - return -} - -func (m *imapRootMailbox) Status(_ []imap.StatusItem) (*imap.MailboxStatus, error) { - status := &imap.MailboxStatus{} - if m.isFolder { - status.Name = store.UserFoldersMailboxName - } else { - status.Name = store.UserLabelsMailboxName - } - return status, nil -} - -func (m *imapRootMailbox) SetSubscribed(_ bool) error { - return errors.New("cannot subscribe or unsubsribe to Labels or Folders mailboxes") -} - -func (m *imapRootMailbox) Check() error { - return nil -} - -func (m *imapRootMailbox) ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error { - close(ch) - return nil -} - -func (m *imapRootMailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { - return -} - -func (m *imapRootMailbox) CreateMessage(flags []string, t time.Time, body imap.Literal) error { - return errors.New("cannot create a message in this mailbox") -} - -func (m *imapRootMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) (err error) { - return errors.New("cannot update message flags in this mailbox") -} - -func (m *imapRootMailbox) CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error { - return nil -} - -// Expunge is not used by Bridge. We delete the message once it is flagged as \Deleted. -func (m *imapRootMailbox) Expunge() error { - return nil -} diff --git a/internal/imap/map.go b/internal/imap/map.go deleted file mode 100644 index 898a6bde..00000000 --- a/internal/imap/map.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import "sync" - -type safeMapOfStrings struct { - data map[string]string - mutex sync.RWMutex -} - -func newSafeMapOfString() safeMapOfStrings { - return safeMapOfStrings{ - data: map[string]string{}, - mutex: sync.RWMutex{}, - } -} - -func (m *safeMapOfStrings) get(key string) string { - m.mutex.RLock() - defer m.mutex.RUnlock() - return m.data[key] -} - -func (m *safeMapOfStrings) set(key, value string) { - m.mutex.Lock() - defer m.mutex.Unlock() - m.data[key] = value -} diff --git a/internal/imap/server.go b/internal/imap/server.go deleted file mode 100644 index c41cec74..00000000 --- a/internal/imap/server.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "crypto/tls" - "fmt" - "io" - "net" - "strings" - "time" - - imapid "github.com/ProtonMail/go-imap-id" - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/useragent" - "github.com/ProtonMail/proton-bridge/v2/internal/imap/id" - "github.com/ProtonMail/proton-bridge/v2/internal/imap/idle" - "github.com/ProtonMail/proton-bridge/v2/internal/imap/uidplus" - "github.com/ProtonMail/proton-bridge/v2/internal/serverutil" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/emersion/go-imap" - imapappendlimit "github.com/emersion/go-imap-appendlimit" - imapmove "github.com/emersion/go-imap-move" - imapquota "github.com/emersion/go-imap-quota" - imapunselect "github.com/emersion/go-imap-unselect" - "github.com/emersion/go-imap/backend" - imapserver "github.com/emersion/go-imap/server" - "github.com/emersion/go-sasl" -) - -// Server takes care of IMAP listening serving. It implements serverutil.Server. -type Server struct { - panicHandler panicHandler - userAgent *useragent.UserAgent - debugClient bool - debugServer bool - port int - - server *imapserver.Server - controller serverutil.Controller -} - -// NewIMAPServer constructs a new IMAP server configured with the given options. -func NewIMAPServer( - panicHandler panicHandler, - debugClient, debugServer bool, - port int, - tls *tls.Config, - imapBackend backend.Backend, - userAgent *useragent.UserAgent, - eventListener listener.Listener, -) *Server { - server := &Server{ - panicHandler: panicHandler, - userAgent: userAgent, - debugClient: debugClient, - debugServer: debugServer, - port: port, - } - - server.server = newGoIMAPServer(tls, imapBackend, server.Address(), userAgent) - server.controller = serverutil.NewController(server, eventListener) - return server -} - -func newGoIMAPServer(tls *tls.Config, backend backend.Backend, address string, userAgent *useragent.UserAgent) *imapserver.Server { - server := imapserver.New(backend) - server.TLSConfig = tls - server.AllowInsecureAuth = true - server.ErrorLog = serverutil.NewServerErrorLogger(serverutil.IMAP) - server.AutoLogout = 30 * time.Minute - server.Addr = address - - serverID := imapid.ID{ - imapid.FieldName: "Proton Mail Bridge", - imapid.FieldVendor: "Proton AG", - imapid.FieldSupportURL: "https://proton.me/support/mail", - } - - server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server { - return sasl.NewLoginServer(func(address, password string) error { - user, err := conn.Server().Backend.Login(nil, address, password) - if err != nil { - return err - } - - ctx := conn.Context() - ctx.State = imap.AuthenticatedState - ctx.User = user - return nil - }) - }) - - server.Enable( - idle.NewExtension(), - imapmove.NewExtension(), - id.NewExtension(serverID, userAgent), - imapquota.NewExtension(), - imapappendlimit.NewExtension(), - imapunselect.NewExtension(), - uidplus.NewExtension(), - ) - - return server -} - -// ListenAndServe will run server and all monitors. -func (s *Server) ListenAndServe() { s.controller.ListenAndServe() } - -// Close turns off server and monitors. -func (s *Server) Close() { s.controller.Close() } - -// Implements serverutil.Server interface. - -func (Server) Protocol() serverutil.Protocol { return serverutil.IMAP } -func (s *Server) UseSSL() bool { return false } -func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) } -func (s *Server) TLSConfig() *tls.Config { return s.server.TLSConfig } -func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() } - -func (s *Server) DebugServer() bool { return s.debugServer } -func (s *Server) DebugClient() bool { return s.debugClient } - -func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) { - s.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug) - - if !s.userAgent.HasClient() { - s.userAgent.SetClient("UnknownClient", "0.0.1") - } -} - -func (s *Server) DisconnectUser(address string) { - log.Info("Disconnecting all open IMAP connections for ", address) - s.server.ForEachConn(func(conn imapserver.Conn) { - connUser := conn.Context().User - if connUser != nil && strings.EqualFold(connUser.Username(), address) { - if err := conn.Close(); err != nil { - log.WithError(err).Error("Failed to close the connection") - } - } - }) -} - -func (s *Server) Serve(listener net.Listener) error { return s.server.Serve(listener) } -func (s *Server) StopServe() error { return s.server.Close() } diff --git a/internal/imap/store.go b/internal/imap/store.go deleted file mode 100644 index d91a4208..00000000 --- a/internal/imap/store.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "io" - "net/mail" - "net/textproto" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/imap/uidplus" - "github.com/ProtonMail/proton-bridge/v2/internal/store" - pkgMsg "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -type storeUserProvider interface { - UserID() string - GetSpaceKB() (usedSpace, maxSpace uint32, err error) - GetMaxUpload() (int64, error) - - GetAddress(addressID string) (storeAddressProvider, error) - - CreateDraft( - kr *crypto.KeyRing, - message *pmapi.Message, - attachmentReaders []io.Reader, - attachedPublicKey, - attachedPublicKeyName string, - parentID string) (*pmapi.Message, []*pmapi.Attachment, error) - - SetChangeNotifier(store.ChangeNotifier) -} - -type storeAddressProvider interface { - AddressString() string - AddressID() string - APIAddress() *pmapi.Address - - CreateMailbox(name string) error - ListMailboxes() []storeMailboxProvider - GetMailbox(name string) (storeMailboxProvider, error) -} - -type storeMailboxProvider interface { - LabelID() string - Name() string - Color() string - IsSystem() bool - IsFolder() bool - IsLabel() bool - UIDValidity() uint32 - - Rename(newName string) error - Delete() error - - GetAPIIDsFromUIDRange(start, stop uint32) ([]string, error) - GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error) - GetLatestAPIID() (string, error) - GetNextUID() (uint32, error) - GetDeletedAPIIDs() ([]string, error) - GetCounts() (dbTotal, dbUnread, dbUnreadSeqNum uint, err error) - GetUIDList(apiIDs []string) *uidplus.OrderedSeq - GetUIDByHeader(header *mail.Header) uint32 - GetDelimiter() string - - GetMessage(apiID string) (storeMessageProvider, error) - LabelMessages(apiID []string) error - UnlabelMessages(apiID []string) error - MarkMessagesRead(apiID []string) error - MarkMessagesUnread(apiID []string) error - MarkMessagesStarred(apiID []string) error - MarkMessagesUnstarred(apiID []string) error - MarkMessagesDeleted(apiID []string) error - MarkMessagesUndeleted(apiID []string) error - ImportMessage(enc []byte, seen bool, labelIDs []string, flags, time int64) (string, error) - RemoveDeleted(apiIDs []string) error -} - -type storeMessageProvider interface { - ID() string - UID() (uint32, error) - SequenceNumber() (uint32, error) - Message() *pmapi.Message - IsMarkedDeleted() bool - - GetHeader() ([]byte, error) - GetRFC822() ([]byte, error) - GetRFC822Size() (uint32, error) - GetMIMEHeaderFast() textproto.MIMEHeader - IsFullHeaderCached() bool - GetBodyStructure() (*pkgMsg.BodyStructure, error) -} - -type storeUserWrap struct { - *store.Store -} - -// newStoreUserWrap wraps store struct into local storeUserWrap to implement local -// interface. The problem is that store returns the store package's Address type, so -// every method that returns an address has to be overridden to fulfill the interface. -// The same is true for other store structs i.e. storeAddress or storeMailbox. -func newStoreUserWrap(store *store.Store) *storeUserWrap { - return &storeUserWrap{Store: store} -} - -func (s *storeUserWrap) GetAddress(addressID string) (storeAddressProvider, error) { - address, err := s.Store.GetAddress(addressID) - if err != nil { - return nil, err - } - return newStoreAddressWrap(address), nil //nolint:typecheck missing methods are inherited -} - -type storeAddressWrap struct { - *store.Address -} - -func newStoreAddressWrap(address *store.Address) *storeAddressWrap { - return &storeAddressWrap{Address: address} -} - -func (s *storeAddressWrap) ListMailboxes() []storeMailboxProvider { - mailboxes := []storeMailboxProvider{} - for _, mailbox := range s.Address.ListMailboxes() { - mailboxes = append(mailboxes, newStoreMailboxWrap(mailbox)) //nolint:typecheck missing methods are inherited - } - return mailboxes -} - -func (s *storeAddressWrap) GetMailbox(name string) (storeMailboxProvider, error) { - mailbox, err := s.Address.GetMailbox(name) - if err != nil { - return nil, err - } - return newStoreMailboxWrap(mailbox), nil //nolint:typecheck missing methods are inherited -} - -type storeMailboxWrap struct { - *store.Mailbox -} - -func newStoreMailboxWrap(mailbox *store.Mailbox) *storeMailboxWrap { - return &storeMailboxWrap{Mailbox: mailbox} -} - -func (s *storeMailboxWrap) GetMessage(apiID string) (storeMessageProvider, error) { - return s.Mailbox.GetMessage(apiID) -} diff --git a/internal/imap/uidplus/extension.go b/internal/imap/uidplus/extension.go deleted file mode 100644 index 9e2285fb..00000000 --- a/internal/imap/uidplus/extension.go +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package uidplus DOES NOT implement full RFC4315! -// -// Excluded parts are: -// - Response `UIDNOTSTICKY`: All mailboxes of Bridge support stable -// UIDVALIDITY so it would never return this response -// -// Otherwise the standard RFC4315 is followed. -package uidplus - -import ( - "errors" - "fmt" - - "github.com/emersion/go-imap" - "github.com/emersion/go-imap/server" -) - -// Capability extension identifier. -const Capability = "UIDPLUS" - -const ( - copyuid = "COPYUID" - appenduid = "APPENDUID" - copySuccess = "COPY completed" - appendSucess = "APPEND completed" -) - -// OrderedSeq to remember Seq in order they are added. -// We didn't find any restriction in RFC that server must respond with ranges -// so we decided to always do explicit list. This makes sure that no dynamic -// ranges or out of the bound ranges are possible. -// -// NOTE: potential issue with response length -// - the user selects large number of messages to be copied and the -// response line will be long, -// - list of UIDs which high values -// -// which can create long response line. We didn't find a maximum length of one -// IMAP response line or maximum length of IMAP "response code" with parameters. -type OrderedSeq []uint32 - -// Len return number of added seq numbers. -func (os OrderedSeq) Len() int { return len(os) } - -// Add number to sequence. Zero is not acceptable UID and it won't be added to list. -func (os *OrderedSeq) Add(num uint32) { - if num == 0 { - return - } - *os = append(*os, num) -} - -func (os *OrderedSeq) String() string { - out := "" - if len(*os) == 0 { - return out - } - - lastS := uint32(0) - isRangeOpened := false - for i, s := range *os { - // write first - if i == 0 { - out += fmt.Sprintf("%d", s) - isRangeOpened = false - lastS = s - continue - } - - isLast := (i == len(*os)-1) - isContinuous := (lastS+1 == s) - - if isContinuous { - isRangeOpened = true - lastS = s - if isLast { - out += fmt.Sprintf(":%d", s) - } - continue - } - - if isRangeOpened && !isContinuous { // close range - out += fmt.Sprintf(":%d,%d", lastS, s) - isRangeOpened = false - lastS = s - continue - } - - // Range is not opened and it is not continuous. - out += fmt.Sprintf(",%d", s) - isRangeOpened = false - lastS = s - } - - return out -} - -// UIDExpunge implements server.Handler but Bridge is not supporting -// UID EXPUNGE with specific UIDs. - -type UIDExpungeMailbox interface { - Expunge() error - UIDExpunge(*imap.SeqSet) error -} - -type UIDExpunge struct { - SeqSet *imap.SeqSet -} - -func newUIDExpunge() *UIDExpunge { - return &UIDExpunge{} -} - -func (e *UIDExpunge) Parse(fields []interface{}) error { - if len(fields) == 0 { - return nil // It could be regular EXPUNGE without arguments. - } - if len(fields) > 1 { - return errors.New("too many arguments") - } - - seqset, ok := fields[0].(string) - if !ok { - return errors.New("sequence set must be an atom") - } - var err error - e.SeqSet, err = imap.ParseSeqSet(seqset) - return err -} - -func (e *UIDExpunge) Handle(conn server.Conn) error { - mailbox, err := e.getMailbox(conn) - if err != nil { - return err - } - return mailbox.Expunge() -} - -func (e *UIDExpunge) UidHandle(conn server.Conn) error { //nolint:revive,stylecheck - if e.SeqSet == nil { - return errors.New("missing sequence set") - } - mailbox, err := e.getMailbox(conn) - if err != nil { - return err - } - return mailbox.UIDExpunge(e.SeqSet) -} - -func (e *UIDExpunge) getMailbox(conn server.Conn) (UIDExpungeMailbox, error) { - ctx := conn.Context() - if ctx.Mailbox == nil { - return nil, server.ErrNoMailboxSelected - } - if ctx.MailboxReadOnly { - return nil, server.ErrMailboxReadOnly - } - - mailbox, ok := ctx.Mailbox.(UIDExpungeMailbox) - if !ok { - return nil, errors.New("UID EXPUNGE is not implemented") - } - return mailbox, nil -} - -type extension struct{} - -// NewExtension of UIDPLUS. -func NewExtension() server.Extension { - return &extension{} -} - -func (ext *extension) Capabilities(c server.Conn) []string { - if c.Context().State&imap.AuthenticatedState != 0 { - return []string{Capability} - } - return nil -} - -func (ext *extension) Command(name string) server.HandlerFactory { - if name == "EXPUNGE" { - return func() server.Handler { - return newUIDExpunge() - } - } - - return nil -} - -func getStatusResponseCopy(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) *imap.StatusResp { - info := copySuccess - - if sourceSeq.Len() != 0 && targetSeq.Len() != 0 && - sourceSeq.Len() == targetSeq.Len() { - info = fmt.Sprintf("[%s %d %s %s] %s", - copyuid, - uidValidity, - sourceSeq.String(), - targetSeq.String(), - copySuccess, - ) - } - - return &imap.StatusResp{ - Type: imap.StatusRespOk, - Info: info, - } -} - -// CopyResponse prepares OK response with extended UID information about copied message. -func CopyResponse(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) error { - return &imap.ErrStatusResp{ - Resp: getStatusResponseCopy(uidValidity, sourceSeq, targetSeq), - } -} - -func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.StatusResp { - info := appendSucess - if targetSeq.Len() > 0 { - info = fmt.Sprintf("[%s %d %s] %s", - appenduid, - uidValidity, - targetSeq.String(), - appendSucess, - ) - } - - return &imap.StatusResp{ - Type: imap.StatusRespOk, - Info: info, - } -} - -// AppendResponse prepares OK response with extended UID information about appended message. -func AppendResponse(uidValidity uint32, targetSeq *OrderedSeq) error { - return &imap.ErrStatusResp{ - Resp: getStatusResponseAppend(uidValidity, targetSeq), - } -} diff --git a/internal/imap/uidplus/extension_test.go b/internal/imap/uidplus/extension_test.go deleted file mode 100644 index 840b0010..00000000 --- a/internal/imap/uidplus/extension_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package uidplus - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -// uidValidity is constant and global for bridge IMAP. -const uidValidity = 66 - -type testResponseData struct { - sourceList, targetList []int - expCopyInfo, expAppendInfo string -} - -func (td *testResponseData) getOrdSeqFromList(seqList []int) *OrderedSeq { - set := &OrderedSeq{} - for _, seq := range seqList { - set.Add(uint32(seq)) - } - return set -} - -func (td *testResponseData) testCopyAndAppendResponses(tb testing.TB) { - sourceSeq := td.getOrdSeqFromList(td.sourceList) - targetSeq := td.getOrdSeqFromList(td.targetList) - - gotCopyResp := getStatusResponseCopy(uidValidity, sourceSeq, targetSeq) - assert.Equal(tb, td.expCopyInfo, gotCopyResp.Info, "source: %v\ntarget: %v", td.sourceList, td.targetList) - - gotAppendResp := getStatusResponseAppend(uidValidity, targetSeq) - assert.Equal(tb, td.expAppendInfo, gotAppendResp.Info, "source: %v\ntarget: %v", td.sourceList, td.targetList) -} - -func TestStatusResponseInfo(t *testing.T) { - testData := []*testResponseData{ - { // Dynamic range must never be returned e.g 4:* (explicitly true if you OrderedSeq used instead of imap.SeqSet). - sourceList: []int{4, 5, 6}, - targetList: []int{1, 2, 3}, - expCopyInfo: "[" + copyuid + " 66 4:6 1:3] " + copySuccess, - expAppendInfo: "[" + appenduid + " 66 1:3] " + appendSucess, - }, - { // Ranges can be used only for consecutive strictly rising sequence. - sourceList: []int{6, 7, 8, 9, 10, 1, 3, 5, 10, 11, 20, 21, 30, 31}, - targetList: []int{1, 2, 3, 4, 50, 8, 7, 6, 12, 13, 22, 23, 32, 33}, - expCopyInfo: "[" + copyuid + " 66 6:10,1,3,5,10:11,20:21,30:31 1:4,50,8,7,6,12:13,22:23,32:33] " + copySuccess, - expAppendInfo: "[" + appenduid + " 66 1:4,50,8,7,6,12:13,22:23,32:33] " + appendSucess, - }, - { // Keep order (cannot use sequence set because 3,2,1 equals 1,2,3 equals 1:3 equals 3:1). - sourceList: []int{4, 5, 8}, - targetList: []int{3, 2, 1}, - expCopyInfo: "[" + copyuid + " 66 4:5,8 3,2,1] " + copySuccess, - expAppendInfo: "[" + appenduid + " 66 3,2,1] " + appendSucess, - }, - { // Incorrect count of source and target uids is wrong and we should not report it. - sourceList: []int{1}, - targetList: []int{1, 2, 3}, - expCopyInfo: copySuccess, - expAppendInfo: "[" + appenduid + " 66 1:3] " + appendSucess, - }, - { - sourceList: []int{1, 2, 3}, - targetList: []int{1}, - expCopyInfo: copySuccess, - expAppendInfo: "[" + appenduid + " 66 1] " + appendSucess, - }, - { // One item should be always interpreted as one number (don't use imap.SeqSet because 1:1 means 1). - sourceList: []int{1}, - targetList: []int{1}, - expCopyInfo: "[" + copyuid + " 66 1 1] " + copySuccess, - expAppendInfo: "[" + appenduid + " 66 1] " + appendSucess, - }, - { // No UID is wrong we should not report it. - sourceList: []int{1}, - targetList: []int{}, - expCopyInfo: copySuccess, - expAppendInfo: appendSucess, - }, - { // Duplicates should be reported as list. - sourceList: []int{1, 1, 1}, - targetList: []int{6, 6, 6}, - expCopyInfo: "[" + copyuid + " 66 1,1,1 6,6,6] " + copySuccess, - expAppendInfo: "[" + appenduid + " 66 6,6,6] " + appendSucess, - }, - } - - for _, td := range testData { - td.testCopyAndAppendResponses(t) - } -} diff --git a/internal/imap/updates.go b/internal/imap/updates.go deleted file mode 100644 index c011510d..00000000 --- a/internal/imap/updates.go +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "strings" - "sync" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/store" - "github.com/ProtonMail/proton-bridge/v2/pkg/algo" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - imap "github.com/emersion/go-imap" - goIMAPBackend "github.com/emersion/go-imap/backend" - "github.com/sirupsen/logrus" -) - -type operation string - -const ( - operationUpdateMessage operation = "store" - operationDeleteMessage operation = "expunge" -) - -type imapUpdates struct { - lock sync.Locker - blocking map[string]bool - delayedExpunges map[string][]chan struct{} - ch chan goIMAPBackend.Update - ib *imapBackend -} - -func newIMAPUpdates(ib *imapBackend) *imapUpdates { - return &imapUpdates{ - lock: &sync.Mutex{}, - blocking: map[string]bool{}, - delayedExpunges: map[string][]chan struct{}{}, - ch: make(chan goIMAPBackend.Update), - ib: ib, - } -} - -func (iu *imapUpdates) block(address, mailboxName string, op operation) { - iu.lock.Lock() - defer iu.lock.Unlock() - - iu.blocking[getBlockingKey(address, mailboxName, op)] = true -} - -func (iu *imapUpdates) unblock(address, mailboxName string, op operation) { - iu.lock.Lock() - defer iu.lock.Unlock() - - delete(iu.blocking, getBlockingKey(address, mailboxName, op)) -} - -func (iu *imapUpdates) isBlocking(address, mailboxName string, op operation) bool { - iu.lock.Lock() - defer iu.lock.Unlock() - - return iu.blocking[getBlockingKey(address, mailboxName, op)] -} - -func getBlockingKey(address, mailboxName string, op operation) string { - return strings.ToLower(address + "_" + mailboxName + "_" + string(op)) -} - -func (iu *imapUpdates) forbidExpunge(mailboxID string) { - iu.lock.Lock() - defer iu.lock.Unlock() - - iu.delayedExpunges[mailboxID] = []chan struct{}{} -} - -func (iu *imapUpdates) allowExpunge(mailboxID string) { - iu.lock.Lock() - defer iu.lock.Unlock() - - for _, ch := range iu.delayedExpunges[mailboxID] { - close(ch) - } - delete(iu.delayedExpunges, mailboxID) -} - -func (iu *imapUpdates) CanDelete(mailboxID string) (bool, func()) { - iu.lock.Lock() - defer iu.lock.Unlock() - - if iu.delayedExpunges[mailboxID] == nil { - return true, nil - } - - ch := make(chan struct{}) - iu.delayedExpunges[mailboxID] = append(iu.delayedExpunges[mailboxID], ch) - return false, func() { - log.WithField("mailbox", mailboxID).Debug("Expunge operations paused") - <-ch - log.WithField("mailbox", mailboxID).Debug("Expunge operations unpaused") - } -} - -func (iu *imapUpdates) Notice(address, notice string) { - l := iu.updateLog(address, "") - l.Info("Notice") - update := new(goIMAPBackend.StatusUpdate) - update.Update = goIMAPBackend.NewUpdate(address, "") - update.StatusResp = &imap.StatusResp{ - Type: imap.StatusRespOk, - Code: imap.CodeAlert, - Info: notice, - } - iu.sendIMAPUpdate(l, update, false) -} - -func (iu *imapUpdates) UpdateMessage( - address, mailboxName string, - uid, sequenceNumber uint32, - msg *pmapi.Message, hasDeletedFlag bool, -) { - l := iu.updateLog(address, mailboxName). - WithFields(logrus.Fields{ - "seqNum": sequenceNumber, - "uid": uid, - "flags": message.GetFlags(msg), - "deleted": hasDeletedFlag, - }) - l.Info("IDLE update") - update := new(goIMAPBackend.MessageUpdate) - update.Update = goIMAPBackend.NewUpdate(address, mailboxName) - update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid}) - update.Message.Flags = message.GetFlags(msg) - if hasDeletedFlag { - update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag) - } - update.Message.Uid = uid - iu.sendIMAPUpdate(l, update, iu.isBlocking(address, mailboxName, operationUpdateMessage)) -} - -func (iu *imapUpdates) DeleteMessage(address, mailboxName string, sequenceNumber uint32) { - l := iu.updateLog(address, mailboxName). - WithField("seqNum", sequenceNumber) - l.Info("IDLE delete") - update := new(goIMAPBackend.ExpungeUpdate) - update.Update = goIMAPBackend.NewUpdate(address, mailboxName) - update.SeqNum = sequenceNumber - iu.sendIMAPUpdate(l, update, iu.isBlocking(address, mailboxName, operationDeleteMessage)) -} - -func (iu *imapUpdates) MailboxCreated(address, mailboxName string) { - l := iu.updateLog(address, mailboxName) - l.Info("IDLE mailbox info") - update := new(goIMAPBackend.MailboxInfoUpdate) - update.Update = goIMAPBackend.NewUpdate(address, "") - update.MailboxInfo = &imap.MailboxInfo{ - Attributes: []string{imap.NoInferiorsAttr}, - Delimiter: store.PathDelimiter, - Name: mailboxName, - } - iu.sendIMAPUpdate(l, update, false) -} - -func (iu *imapUpdates) MailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint32) { - l := iu.updateLog(address, mailboxName). - WithFields(logrus.Fields{ - "total": total, - "unread": unread, - "unreadSeqNum": unreadSeqNum, - }) - l.Info("IDLE status") - update := new(goIMAPBackend.MailboxUpdate) - update.Update = goIMAPBackend.NewUpdate(address, mailboxName) - update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen}) - update.MailboxStatus.Messages = total - update.MailboxStatus.Unseen = unread - update.MailboxStatus.UnseenSeqNum = unreadSeqNum - iu.sendIMAPUpdate(l, update, true) -} - -func (iu *imapUpdates) sendIMAPUpdate(updateLog *logrus.Entry, update goIMAPBackend.Update, isBlocking bool) { - l := updateLog.WithField("blocking", isBlocking) - if iu.ch == nil { - l.Info("IMAP IDLE unavailable") - return - } - - done := update.Done() - go func() { - select { - case <-time.After(1 * time.Second): - l.Warn("IMAP update could not be sent (timeout)") - return - case iu.ch <- update: - } - }() - - if !isBlocking { - return - } - - select { - case <-done: - case <-time.After(1 * time.Second): - l.Warn("IMAP update could not be delivered (timeout)") - return - } -} - -func (iu *imapUpdates) getIDs(address, mailboxName string) (addressID, mailboxID string) { - addressID = "unknown-" + algo.HashBase64SHA256(address) - mailboxID = "unknown-" + algo.HashBase64SHA256(mailboxName) - - if iu == nil || iu.ib == nil { - return - } - - user, err := iu.ib.getUser(address) - if err != nil || user == nil || user.storeAddress == nil { - return - } - addressID = user.addressID - - if v := user.mailboxIDs.get(mailboxName); v != "" { - mailboxID = v - } - - return -} - -func (iu *imapUpdates) updateLog(address, mailboxName string) *logrus.Entry { - addressID, mailboxID := iu.getIDs(address, mailboxName) - return log. - WithField("address", addressID). - WithField("mailbox", mailboxID) -} diff --git a/internal/imap/updates_test.go b/internal/imap/updates_test.go deleted file mode 100644 index 7fac2d3d..00000000 --- a/internal/imap/updates_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestUpdatesCanDelete(t *testing.T) { - u := newIMAPUpdates(nil) - - can, _ := u.CanDelete("mbox") - require.True(t, can) - - u.forbidExpunge("mbox") - u.allowExpunge("mbox") - - can, _ = u.CanDelete("mbox") - require.True(t, can) -} - -func TestUpdatesCannotDelete(t *testing.T) { - u := newIMAPUpdates(nil) - - u.forbidExpunge("mbox") - can, wait := u.CanDelete("mbox") - require.False(t, can) - - ch := make(chan time.Duration) - go func() { - start := time.Now() - wait() - ch <- time.Since(start) - close(ch) - }() - - time.Sleep(200 * time.Millisecond) - u.allowExpunge("mbox") - duration := <-ch - - require.True(t, duration > 200*time.Millisecond) -} diff --git a/internal/imap/user.go b/internal/imap/user.go deleted file mode 100644 index eec20570..00000000 --- a/internal/imap/user.go +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package imap - -import ( - "errors" - "strings" - "sync" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - imapquota "github.com/emersion/go-imap-quota" - goIMAPBackend "github.com/emersion/go-imap/backend" -) - -type imapUser struct { - panicHandler panicHandler - backend *imapBackend - user bridgeUser - - storeUser storeUserProvider - storeAddress storeAddressProvider - - currentAddressLowercase string - - // Some clients, for example Outlook, do MOVE by STORE \Deleted, APPEND, - // EXPUNGE where APPEN and EXPUNGE can go in parallel. Usual IMAP servers - // do not deduplicate messages and this it's not an issue, but for APPEND - // for PM means just assigning label. That would cause to assign label and - // then delete the message, or in other words cause data loss. - // go-imap does not call CreateMessage till it gets the whole message from - // IMAP client, therefore with big message, simple wait for APPEND before - // performing EXPUNGE is not enough. There has to be two-way lock. Only - // that way even if EXPUNGE is called few ms before APPEND and message - // is deleted, APPEND will not just assing label but creates the message - // again. - // The issue is only when moving message from folder which is causing - // real removal, so Trash and Spam. Those only need to use the lock to - // not cause huge slow down as EXPUNGE is implicitly called also after - // UNSELECT, CLOSE, or LOGOUT. - appendExpungeLock sync.Mutex - - addressID string // cached value for logs to avoid lock - mailboxIDs safeMapOfStrings // cached values for logs to avoid lock -} - -// newIMAPUser returns struct implementing go-imap/user interface. -func newIMAPUser( - panicHandler panicHandler, - backend *imapBackend, - user bridgeUser, - addressID, address string, -) (*imapUser, error) { - log.WithField("address", addressID).Debug("Creating new IMAP user") - - storeUser := user.GetStore() - if storeUser == nil { - return nil, errors.New("user database is not initialized") - } - - storeAddress, err := storeUser.GetAddress(addressID) - if err != nil { - log.WithField("address", addressID).Debug("Could not get store user address") - return nil, err - } - - return &imapUser{ - panicHandler: panicHandler, - backend: backend, - user: user, - - storeUser: storeUser, - storeAddress: storeAddress, - - currentAddressLowercase: strings.ToLower(address), - addressID: addressID, - mailboxIDs: newSafeMapOfString(), - }, err -} - -// This method should eventually no longer be necessary. Everything should go via store. -func (iu *imapUser) client() pmapi.Client { - return iu.user.GetClient() -} - -func (iu *imapUser) isSubscribed(labelID string) bool { - subscriptionExceptions := iu.backend.getCacheList(iu.storeUser.UserID(), SubscriptionException) - exceptions := strings.Split(subscriptionExceptions, ";") - - for _, exception := range exceptions { - if exception == labelID { - return false - } - } - return true -} - -func (iu *imapUser) removeFromCache(label, value string) { - iu.backend.removeFromCache(iu.storeUser.UserID(), label, value) -} - -func (iu *imapUser) addToCache(label, value string) { - iu.backend.addToCache(iu.storeUser.UserID(), label, value) -} - -// Username returns this user's username. -func (iu *imapUser) Username() string { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - return iu.storeAddress.AddressString() -} - -// ListMailboxes returns a list of mailboxes belonging to this user. -// If subscribed is set to true, returns only subscribed mailboxes. -func (iu *imapUser) ListMailboxes(showOnlySubcribed bool) ([]goIMAPBackend.Mailbox, error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - mailboxes := []goIMAPBackend.Mailbox{} - for _, storeMailbox := range iu.storeAddress.ListMailboxes() { - iu.mailboxIDs.set(storeMailbox.Name(), storeMailbox.LabelID()) - - if storeMailbox.LabelID() == pmapi.AllMailLabel && !iu.backend.bridge.IsAllMailVisible() { - continue - } - - if showOnlySubcribed && !iu.isSubscribed(storeMailbox.LabelID()) { - continue - } - mailbox := newIMAPMailbox(iu.panicHandler, iu, storeMailbox) - mailboxes = append(mailboxes, mailbox) - } - - mailboxes = append(mailboxes, newLabelsRootMailbox()) - mailboxes = append(mailboxes, newFoldersRootMailbox()) - - log.WithField("mailboxes", mailboxes).Trace("Listing mailboxes") - - return mailboxes, nil -} - -// GetMailbox returns a mailbox. -func (iu *imapUser) GetMailbox(name string) (mb goIMAPBackend.Mailbox, err error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - storeMailbox, err := iu.storeAddress.GetMailbox(name) - if err != nil { - logMsg := log.WithField("name", name).WithError(err) - - // GODT-97: some clients perform SELECT "" in order to unselect. - // We don't want to fill the logs with errors in this case. - if name != "" { - logMsg.Error("Could not get mailbox") - } else { - logMsg.Debug("Failed attempt to get mailbox with empty name") - } - - return - } - - return newIMAPMailbox(iu.panicHandler, iu, storeMailbox), nil -} - -// CreateMailbox creates a new mailbox. -func (iu *imapUser) CreateMailbox(name string) error { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - return iu.storeAddress.CreateMailbox(name) -} - -// DeleteMailbox permanently removes the mailbox with the given name. -func (iu *imapUser) DeleteMailbox(name string) (err error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - storeMailbox, err := iu.storeAddress.GetMailbox(name) - if err != nil { - log.WithField("name", name).WithError(err).Error("Could not get mailbox") - return - } - - return storeMailbox.Delete() -} - -// RenameMailbox changes the name of a mailbox. It is an error to attempt to -// rename a mailbox that does not exist or to rename a mailbox to a name that -// already exists. -func (iu *imapUser) RenameMailbox(oldName, newName string) (err error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - storeMailbox, err := iu.storeAddress.GetMailbox(oldName) - if err != nil { - log.WithField("name", oldName).WithError(err).Error("Could not get mailbox") - return - } - - return storeMailbox.Rename(newName) -} - -// Logout is called when this User will no longer be used, likely because the -// client closed the connection. -func (iu *imapUser) Logout() (err error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - log.Debug("IMAP client logged out address ", iu.storeAddress.AddressID()) - - iu.backend.deleteUser(iu.currentAddressLowercase) - - return nil -} - -func (iu *imapUser) GetQuota(name string) (*imapquota.Status, error) { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - usedSpace, maxSpace, err := iu.storeUser.GetSpaceKB() - if err != nil { - log.Error("Failed getting quota: ", err) - return nil, err - } - - resources := make(map[string][2]uint32) - var list [2]uint32 - list[0] = usedSpace - list[1] = maxSpace - resources[imapquota.ResourceStorage] = list - status := &imapquota.Status{ - Name: "", - Resources: resources, - } - - return status, nil -} - -func (iu *imapUser) SetQuota(name string, resources map[string]uint32) error { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - return errors.New("quota cannot be set") -} - -func (iu *imapUser) CreateMessageLimit() *uint32 { - // Called from go-imap in goroutines - we need to handle panics for each function. - defer iu.panicHandler.HandlePanic() - - maxUpload, err := iu.storeUser.GetMaxUpload() - if err != nil { - log.Error("Failed getting current user for message limit: ", err) - zero := uint32(0) - return &zero - } - - upload := uint32(maxUpload) - return &upload -} diff --git a/internal/locations/locations.go b/internal/locations/locations.go index 69e528f8..6382d68a 100644 --- a/internal/locations/locations.go +++ b/internal/locations/locations.go @@ -51,11 +51,6 @@ func New(provider Provider, configName string) *Locations { } } -// GetLockFile returns the path to the lock file (e.g. ~/.cache///.lock). -func (l *Locations) GetLockFile() string { - return filepath.Join(l.userCache, l.configName+".lock") -} - // GetGuiLockFile returns the path to the lock file (e.g. ~/.cache///.lock). func (l *Locations) GetGuiLockFile() string { return filepath.Join(l.userCache, l.configGuiName+".lock") @@ -127,6 +122,16 @@ func (l *Locations) ProvideSettingsPath() (string, error) { return l.getSettingsPath(), nil } +// ProvideGluonPath returns a location for gluon data. +// It creates it if it doesn't already exist. +func (l *Locations) ProvideGluonPath() (string, error) { + if err := os.MkdirAll(l.getGluonPath(), 0o700); err != nil { + return "", err + } + + return l.getGluonPath(), nil +} + // ProvideLogsPath returns a location for user logs (e.g. ~/.cache///logs). // It creates it if it doesn't already exist. func (l *Locations) ProvideLogsPath() (string, error) { @@ -137,19 +142,14 @@ func (l *Locations) ProvideLogsPath() (string, error) { return l.getLogsPath(), nil } -// ProvideCachePath returns a location for user cache dirs (e.g. ~/.config///cache). +// ProvideGUICertPath returns a location for TLS certs used for the connection between bridge and the GUI. // It creates it if it doesn't already exist. -func (l *Locations) ProvideCachePath() (string, error) { - if err := os.MkdirAll(l.getCachePath(), 0o700); err != nil { +func (l *Locations) ProvideGUICertPath() (string, error) { + if err := os.MkdirAll(l.getGUICertPath(), 0o700); err != nil { return "", err } - return l.getCachePath(), nil -} - -// GetOldCachePath returns a former location for user cache dirs used for migration scripts only. -func (l *Locations) GetOldCachePath() string { - return filepath.Join(l.userCache, "cache") + return l.getGUICertPath(), nil } // ProvideUpdatesPath returns a location for update files (e.g. ~/.cache///updates). @@ -172,6 +172,14 @@ func (l *Locations) GetOldUpdatesPath() string { return filepath.Join(l.userCache, "updates") } +func (l *Locations) getGluonPath() string { + return filepath.Join(l.userCache, "gluon") +} + +func (l *Locations) getGUICertPath() string { + return l.userConfig +} + func (l *Locations) getSettingsPath() string { return l.userConfig } @@ -180,22 +188,6 @@ func (l *Locations) getLogsPath() string { return filepath.Join(l.userCache, "logs") } -func (l *Locations) getCachePath() string { - // Bridge cache is not a typical cache which can be deleted with only - // downside that the app has to download everything again. - // Cache for bridge is database with IMAP UIDs and UIDVALIDITY, and also - // other IMAP setup. Deleting such data leads to either re-sync of client, - // or mix of headers and bodies. Both is caused because of need of re-sync - // between Bridge and API which will happen in different order than before. - // In the first case, UIDVALIDITY is also changed and causes the better - // outcome to "just" re-sync everything; in the later, UIDVALIDITY stays - // the same, causing the client to not re-sync but UIDs in the client does - // not match UIDs in Bridge. - // Because users might use tools to regularly clear caches, Bridge cache - // cannot be located in a standard cache folder. - return filepath.Join(l.userConfig, "cache") -} - func (l *Locations) getUpdatesPath() string { // In order to properly update Bridge 1.6.X and higher we need to // change the launcher first. Since this is not part of automatic @@ -216,7 +208,6 @@ func (l *Locations) Clear() error { l.userConfig, l.userCache, ).Except( - l.GetLockFile(), l.GetGuiLockFile(), l.getUpdatesPath(), ).Do() @@ -233,10 +224,8 @@ func (l *Locations) ClearUpdates() error { // while leaving files in the standard locations untouched. func (l *Locations) Clean() error { return files.Remove(l.userCache).Except( - l.GetLockFile(), l.GetGuiLockFile(), l.getLogsPath(), - l.getCachePath(), l.getUpdatesPath(), ).Do() } diff --git a/internal/locations/locations_test.go b/internal/locations/locations_test.go index 2b430fdc..ca147beb 100644 --- a/internal/locations/locations_test.go +++ b/internal/locations/locations_test.go @@ -43,11 +43,9 @@ func TestClearRemovesEverythingExceptLockAndUpdateFiles(t *testing.T) { assert.NoError(t, l.Clear()) - assert.FileExists(t, l.GetLockFile()) assert.DirExists(t, l.getSettingsPath()) assert.NoFileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json")) assert.NoDirExists(t, l.getLogsPath()) - assert.NoDirExists(t, l.getCachePath()) assert.DirExists(t, l.getUpdatesPath()) } @@ -56,11 +54,9 @@ func TestClearUpdateFiles(t *testing.T) { assert.NoError(t, l.ClearUpdates()) - assert.FileExists(t, l.GetLockFile()) assert.DirExists(t, l.getSettingsPath()) assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json")) assert.DirExists(t, l.getLogsPath()) - assert.DirExists(t, l.getCachePath()) assert.NoDirExists(t, l.getUpdatesPath()) } @@ -74,13 +70,11 @@ func TestCleanLeavesStandardLocationsUntouched(t *testing.T) { assert.NoError(t, l.Clean()) - assert.FileExists(t, l.GetLockFile()) assert.DirExists(t, l.getSettingsPath()) assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json")) assert.DirExists(t, l.getLogsPath()) assert.FileExists(t, filepath.Join(l.getLogsPath(), "log1.txt")) assert.FileExists(t, filepath.Join(l.getLogsPath(), "log2.txt")) - assert.DirExists(t, l.getCachePath()) assert.DirExists(t, l.getUpdatesPath()) } @@ -103,10 +97,8 @@ func TestCleanRemovesUnexpectedFilesAndFolders(t *testing.T) { assert.NoError(t, l.Clean()) - assert.FileExists(t, l.GetLockFile()) assert.DirExists(t, l.getSettingsPath()) assert.DirExists(t, l.getLogsPath()) - assert.DirExists(t, l.getCachePath()) assert.DirExists(t, l.getUpdatesPath()) assert.NoFileExists(t, filepath.Join(l.userCache, "unexpected1.txt")) @@ -117,25 +109,15 @@ func TestCleanRemovesUnexpectedFilesAndFolders(t *testing.T) { } func newFakeAppDirs(t *testing.T) *fakeAppDirs { - configDir, err := os.MkdirTemp("", "test-locations-config") - require.NoError(t, err) - - cacheDir, err := os.MkdirTemp("", "test-locations-cache") - require.NoError(t, err) - return &fakeAppDirs{ - configDir: configDir, - cacheDir: cacheDir, + configDir: t.TempDir(), + cacheDir: t.TempDir(), } } func newTestLocations(t *testing.T) *Locations { l := New(newFakeAppDirs(t), "configName") - lock := l.GetLockFile() - createFilesInDir(t, "", lock) - require.FileExists(t, lock) - settings, err := l.ProvideSettingsPath() require.NoError(t, err) require.DirExists(t, settings) @@ -147,10 +129,6 @@ func newTestLocations(t *testing.T) *Locations { require.NoError(t, err) require.DirExists(t, logs) - cache, err := l.ProvideCachePath() - require.NoError(t, err) - require.DirExists(t, cache) - updates, err := l.ProvideUpdatesPath() require.NoError(t, err) require.DirExists(t, updates) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 1e573648..e5e0a501 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -45,12 +45,13 @@ const ( MaxLogs = 3 ) -func Init(logsPath string) error { +func Init(logsPath, level string) error { logrus.SetFormatter(&logrus.TextFormatter{ ForceColors: true, FullTimestamp: true, TimestampFormat: time.StampMilli, }) + logrus.AddHook(&writer.Hook{ Writer: os.Stderr, LogLevels: []logrus.Level{ @@ -74,24 +75,34 @@ func Init(logsPath string) error { } logrus.SetOutput(rotator) - return nil + + return setLevel(level) } -// SetLevel will change the level of logging and in case of Debug or Trace +// setLevel will change the level of logging and in case of Debug or Trace // level it will also prevent from writing to file. Setting level to Info or // higher will not set writing to file again if it was previously cancelled by // Debug or Trace. -func SetLevel(level string) { - if lvl, err := logrus.ParseLevel(level); err == nil { - logrus.SetLevel(lvl) +func setLevel(level string) error { + if level == "" { + return nil } + logLevel, err := logrus.ParseLevel(level) + if err != nil { + return err + } + + logrus.SetLevel(logLevel) + + // The hook to print panic, fatal and error to stderr is always + // added. We want to avoid log duplicates by replacing all hooks. if logrus.GetLevel() == logrus.DebugLevel || logrus.GetLevel() == logrus.TraceLevel { - // The hook to print panic, fatal and error to stderr is always - // added. We want to avoid log duplicates by replacing all hooks _ = logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{}) logrus.SetOutput(os.Stderr) } + + return nil } func getLogName(version, revision string) string { diff --git a/internal/logging/logging_test.go b/internal/logging/logging_test.go index 047d3551..725a3389 100644 --- a/internal/logging/logging_test.go +++ b/internal/logging/logging_test.go @@ -27,8 +27,7 @@ import ( // TestClearLogs tests that cearLogs removes only bridge old log files keeping last three of them. func TestClearLogs(t *testing.T) { - dir, err := os.MkdirTemp("", "clear-logs-test") - require.NoError(t, err) + dir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dir, "other.log"), []byte("Hello"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(dir, "v1_10.log"), []byte("Hello"), 0o755)) diff --git a/internal/logging/rotator_test.go b/internal/logging/rotator_test.go index a4f2d723..ec09d730 100644 --- a/internal/logging/rotator_test.go +++ b/internal/logging/rotator_test.go @@ -75,23 +75,8 @@ func TestRotator(t *testing.T) { assert.Equal(t, 4, n) } -func BenchmarkRotateRAMFile(b *testing.B) { - dir, err := os.MkdirTemp("", "rotate-benchmark") - require.NoError(b, err) - defer os.RemoveAll(dir) //nolint:errcheck - - benchRotate(b, MaxLogSize, getTestFile(b, dir, MaxLogSize-1)) -} - -func BenchmarkRotateDiskFile(b *testing.B) { - cache, err := os.UserCacheDir() - require.NoError(b, err) - - dir, err := os.MkdirTemp(cache, "rotate-benchmark") - require.NoError(b, err) - defer os.RemoveAll(dir) //nolint:errcheck - - benchRotate(b, MaxLogSize, getTestFile(b, dir, MaxLogSize-1)) +func BenchmarkRotate(b *testing.B) { + benchRotate(b, MaxLogSize, getTestFile(b, b.TempDir(), MaxLogSize-1)) } func benchRotate(b *testing.B, logSize int, getFile func() (io.WriteCloser, error)) { diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go deleted file mode 100644 index a4854f43..00000000 --- a/internal/metrics/metrics.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package metrics collects string constants used to report anonymous usage metrics. -package metrics - -type ( - Category string - Action string - Label string -) - -// Metric represents a single metric that can be reported and contains the necessary fields -// of category, action and label that the /metrics endpoint expects. -type Metric struct { - c Category - a Action - l Label -} - -// New returns a metric struct with the given category, action and label. -// Maybe in future we could perform checks here that the correct category is given for each action. -// That's why the Metric fields are not exported; we don't want users creating broken metrics -// (though for now they still can do that). -func New(c Category, a Action, l Label) Metric { - return Metric{c: c, a: a, l: l} -} - -// Get returns the category, action and label of a metric. -func (m Metric) Get() (Category, Action, Label) { - return m.c, m.a, m.l -} - -// Metrics related to bridge/account setup. -const ( - // Setup is used to group metrics related to bridge setup e.g. first start, new user. - Setup = Category("setup") - - // FirstStart signifies that the bridge has been started for the first time on a user's - // machine (or at least, no config directory was found). - FirstStart = Action("first_start") - - // NewUser signifies a new user account has been added to the bridge. - NewUser = Action("new_user") -) - -// Metrics related to heartbeats of various kinds. -const ( - // Heartbeat is used to group heartbeat metrics e.g. the daily alive signal. - Heartbeat = Category("heartbeat") - - // Daily is a daily signal that indicates continued bridge usage. - Daily = Action("daily") -) - -// Metrics related to import-export (transfer) process. -const ( - // Import is used to group import metrics. - Import = Category("import") - - // Export is used to group export metrics. - Export = Category("export") - - // TransferLoad signifies that the transfer load source. - // It can be IMAP or local files for import, or PM for export. - // With this will be reported also label with number of source mailboxes. - TransferLoad = Action("load") - - // TransferStart signifies started transfer. - TransferStart = Action("start") - - // TransferComplete signifies completed transfer without crash. - TransferComplete = Action("complete") - - // TransferCancel signifies cancelled transfer by an user. - TransferCancel = Action("cancel") - - // TransferFail signifies stopped transfer because of an fatal error. - TransferFail = Action("fail") -) - -const NoLabel = Label("") diff --git a/internal/pool/pool.go b/internal/pool/pool.go new file mode 100644 index 00000000..5b1155ed --- /dev/null +++ b/internal/pool/pool.go @@ -0,0 +1,177 @@ +package pool + +import ( + "context" + "errors" + "sync" + + "github.com/ProtonMail/gluon/queue" +) + +// ErrJobCancelled indicates the job was cancelled. +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 { + queue *queue.QueuedChannel[*Job[In, Out]] + size int +} + +// DoneFunc must be called to free up pool resources. +type DoneFunc func() + +// New returns a new pool. +func New[In comparable, Out any](size int, work func(context.Context, In) (Out, error)) *Pool[In, Out] { + queue := queue.NewQueuedChannel[*Job[In, Out]](0, 0) + + for i := 0; i < size; i++ { + go func() { + for job := range queue.GetChannel() { + select { + case <-job.ctx.Done(): + job.postFailure(ErrJobCancelled) + + default: + res, err := work(job.ctx, job.req) + if err != nil { + job.postFailure(err) + } else { + job.postSuccess(res) + } + + job.waitDone() + } + } + }() + } + + return &Pool[In, Out]{ + queue: queue, + size: size, + } +} + +// NewJob submits a job to the pool. It returns a job handle and a DoneFunc. +// The job handle allows the job result to be obtained. The DoneFunc is used to mark the job as done, +// which frees up the worker in the pool for reuse. +func (pool *Pool[In, Out]) NewJob(ctx context.Context, req In) (*Job[In, Out], DoneFunc) { + job := newJob[In, Out](ctx, req) + + pool.queue.Enqueue(job) + + return job, func() { close(job.done) } +} + +// Process submits jobs to the pool. The callback provides access to the result, or an error if one occurred. +func (pool *Pool[In, Out]) Process(ctx context.Context, reqs []In, fn func(In, Out, error) error) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + var ( + wg sync.WaitGroup + errList []error + lock sync.Mutex + ) + + for _, req := range reqs { + req := req + + wg.Add(1) + + go func() { + defer wg.Done() + + job, done := pool.NewJob(ctx, req) + defer done() + + res, err := job.Result() + + if err := fn(req, res, err); err != nil { + lock.Lock() + defer lock.Unlock() + + // Cancel ongoing jobs. + cancel() + + // Collect the error. + errList = append(errList, err) + } + }() + } + + wg.Wait() + + // TODO: Join the errors somehow? + if len(errList) > 0 { + return errList[0] + } + + return nil +} + +// ProcessAll submits jobs to the pool. All results are returned once available. +func (pool *Pool[In, Out]) ProcessAll(ctx context.Context, reqs []In) (map[In]Out, error) { + var ( + data = make(map[In]Out) + lock = sync.Mutex{} + ) + + if err := pool.Process(ctx, reqs, func(req In, res Out, err error) error { + if err != nil { + return err + } + + lock.Lock() + defer lock.Unlock() + + data[req] = res + + return nil + }); err != nil { + return nil, err + } + + return data, nil +} + +func (pool *Pool[In, Out]) Done() { + pool.queue.Close() +} + +type Job[In, Out any] struct { + ctx context.Context + req In + + res chan Out + err chan error + + done chan struct{} +} + +func newJob[In, Out any](ctx context.Context, req In) *Job[In, Out] { + return &Job[In, Out]{ + ctx: ctx, + req: req, + res: make(chan Out), + err: make(chan error), + done: make(chan struct{}), + } +} + +func (job *Job[In, Out]) Result() (Out, error) { + return <-job.res, <-job.err +} + +func (job *Job[In, Out]) postSuccess(res Out) { + close(job.err) + job.res <- res +} + +func (job *Job[In, Out]) postFailure(err error) { + close(job.res) + job.err <- err +} + +func (job *Job[In, Out]) waitDone() { + <-job.done +} diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go new file mode 100644 index 00000000..a59f3941 --- /dev/null +++ b/internal/pool/pool_test.go @@ -0,0 +1,163 @@ +package pool + +import ( + "context" + "errors" + "runtime" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPool_NewJob(t *testing.T) { + doubler := newDoubler(runtime.NumCPU()) + + job1, done1 := doubler.NewJob(context.Background(), 1) + defer done1() + + job2, done2 := doubler.NewJob(context.Background(), 2) + defer done2() + + res2, err := job2.Result() + require.NoError(t, err) + + res1, err := job1.Result() + require.NoError(t, err) + + assert.Equal(t, 2, res1) + assert.Equal(t, 4, res2) +} + +func TestPool_NewJob_Done(t *testing.T) { + // Create a doubler pool with 2 workers. + doubler := newDoubler(2) + + // Start two jobs. Don't mark the jobs as done yet. + job1, done1 := doubler.NewJob(context.Background(), 1) + job2, done2 := doubler.NewJob(context.Background(), 2) + + // Get the first result. + res1, _ := job1.Result() + assert.Equal(t, 2, res1) + + // Get the first result. + res2, _ := job2.Result() + assert.Equal(t, 4, res2) + + // Additional jobs will wait. + job3, _ := doubler.NewJob(context.Background(), 3) + job4, _ := doubler.NewJob(context.Background(), 4) + + // Channel to collect results from jobs 3 and 4. + resCh := make(chan int, 2) + + go func() { + res, _ := job3.Result() + resCh <- res + }() + + go func() { + res, _ := job4.Result() + resCh <- res + }() + + // Mark jobs 1 and 2 as done, freeing up the workers. + done1() + done2() + + assert.ElementsMatch(t, []int{6, 8}, []int{<-resCh, <-resCh}) +} + +func TestPool_Process(t *testing.T) { + doubler := newDoubler(runtime.NumCPU()) + + var ( + res = make(map[int]int) + lock sync.Mutex + ) + + require.NoError(t, doubler.Process(context.Background(), []int{1, 2, 3, 4, 5}, func(reqVal, resVal int, err error) error { + require.NoError(t, err) + + lock.Lock() + defer lock.Unlock() + + res[reqVal] = resVal + + return nil + })) + + assert.Equal(t, map[int]int{ + 1: 2, + 2: 4, + 3: 6, + 4: 8, + 5: 10, + }, res) +} + +func TestPool_Process_Error(t *testing.T) { + doubler := newDoublerWithError(runtime.NumCPU()) + + assert.Error(t, doubler.Process(context.Background(), []int{1, 2, 3, 4, 5}, func(_ int, _ int, err error) error { + return err + })) +} + +func TestPool_Process_Parallel(t *testing.T) { + doubler := newDoubler(runtime.NumCPU(), 100*time.Millisecond) + + var wg sync.WaitGroup + + for i := 0; i < 8; i++ { + wg.Add(1) + + go func() { + defer wg.Done() + + require.NoError(t, doubler.Process(context.Background(), []int{1, 2, 3, 4}, func(_ int, _ int, err error) error { + return nil + })) + }() + } + + wg.Wait() +} + +func TestPool_ProcessAll(t *testing.T) { + doubler := newDoubler(runtime.NumCPU()) + + res, err := doubler.ProcessAll(context.Background(), []int{1, 2, 3, 4, 5}) + require.NoError(t, err) + + assert.Equal(t, map[int]int{ + 1: 2, + 2: 4, + 3: 6, + 4: 8, + 5: 10, + }, res) +} + +func newDoubler(workers int, delay ...time.Duration) *Pool[int, int] { + return New(workers, func(ctx context.Context, req int) (int, error) { + if len(delay) > 0 { + time.Sleep(delay[0]) + } + + return 2 * req, nil + }) +} + +func newDoublerWithError(workers int) *Pool[int, int] { + return New(workers, func(ctx context.Context, req int) (int, error) { + if req%2 == 0 { + return 0, errors.New("oops") + } + + return 2 * req, nil + }) +} diff --git a/internal/sentry/hostarch_darwin.go b/internal/sentry/hostarch_darwin.go index 6b77467b..955b24cb 100644 --- a/internal/sentry/hostarch_darwin.go +++ b/internal/sentry/hostarch_darwin.go @@ -27,7 +27,7 @@ import ( const translatedProcDarwin = "sysctl.proc_translated" -func getHostAarch() string { +func getHostArch() string { host, err := sysinfo.Host() if err != nil { return "not-detected" diff --git a/internal/sentry/hostarch_default.go b/internal/sentry/hostarch_default.go index 7292e0b9..3ac0051e 100644 --- a/internal/sentry/hostarch_default.go +++ b/internal/sentry/hostarch_default.go @@ -22,7 +22,7 @@ package sentry import "github.com/elastic/go-sysinfo" -func getHostAarch() string { +func getHostArch() string { host, err := sysinfo.Host() if err != nil { return "not-detected" diff --git a/internal/sentry/reporter.go b/internal/sentry/reporter.go index 1aeb876a..c16764d8 100644 --- a/internal/sentry/reporter.go +++ b/internal/sentry/reporter.go @@ -26,7 +26,6 @@ import ( "time" "github.com/ProtonMail/proton-bridge/v2/internal/constants" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" "github.com/getsentry/sentry-go" "github.com/sirupsen/logrus" ) @@ -56,17 +55,21 @@ func init() { //nolint:gochecknoinits type Reporter struct { appName string appVersion string - userAgent fmt.Stringer + identifier Identifier hostArch string } +type Identifier interface { + GetUserAgent() string +} + // NewReporter creates new sentry reporter with appName and appVersion to report. -func NewReporter(appName, appVersion string, userAgent fmt.Stringer) *Reporter { +func NewReporter(appName, appVersion string, identifier Identifier) *Reporter { return &Reporter{ appName: appName, appVersion: appVersion, - userAgent: userAgent, - hostArch: getHostAarch(), + identifier: identifier, + hostArch: getHostArch(), } } @@ -118,7 +121,7 @@ func (r *Reporter) scopedReport(context map[string]interface{}, doReport func()) "OS": runtime.GOOS, "Client": r.appName, "Version": r.appVersion, - "UserAgent": r.userAgent.String(), + "UserAgent": r.identifier.GetUserAgent(), "HostArch": r.hostArch, } @@ -189,6 +192,3 @@ func isFunctionFilteredOut(function string) bool { func Flush(maxWaiTime time.Duration) { sentry.Flush(maxWaiTime) } - -func (r *Reporter) SetClientFromManager(cm pmapi.Manager) { -} diff --git a/internal/serverutil/controller.go b/internal/serverutil/controller.go deleted file mode 100644 index 3e043181..00000000 --- a/internal/serverutil/controller.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package serverutil - -import ( - "crypto/tls" - "fmt" - "net" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/sirupsen/logrus" -) - -// Controller will make sure that server is listening and serving and if needed -// users are disconnected. -type Controller interface { - ListenAndServe() - Close() -} - -// NewController return simple server controller. -func NewController(s Server, l listener.Listener) Controller { - log := logrus.WithField("pkg", "serverutil").WithField("protocol", s.Protocol()) - c := &controller{ - server: s, - signals: l, - log: log, - closeDisconnectUsers: make(chan void), - } - - if s.DebugServer() { - fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") - log.Warning("================================================") - log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") - log.Warning("================================================") - } - - return c -} - -type void struct{} - -type controller struct { - server Server - signals listener.Listener - log *logrus.Entry - - closeDisconnectUsers chan void -} - -func (c *controller) Close() { - c.closeDisconnectUsers <- void{} - if err := c.server.StopServe(); err != nil { - c.log.WithError(err).Error("Issue when closing server") - } -} - -// ListenAndServe starts the server and keeps it on based on internet -// availability. It also monitors and disconnect users if requested. -func (c *controller) ListenAndServe() { - go monitorDisconnectedUsers(c.server, c.signals, c.closeDisconnectUsers) - - defer c.server.HandlePanic() - - l := c.log.WithField("useSSL", c.server.UseSSL()). - WithField("address", c.server.Address()) - - var listener net.Listener - var err error - - if c.server.UseSSL() { - listener, err = tls.Listen("tcp", c.server.Address(), c.server.TLSConfig()) - } else { - listener, err = net.Listen("tcp", c.server.Address()) - } - - if err != nil { - l.WithError(err).Error("Cannot start listener.") - c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error()) - return - } - - // When starting the Bridge, we don't want to retry to notify user - // quickly about the issue. Very probably retry will not help anyway. - l.Info("Starting server") - err = c.server.Serve(&connListener{listener, c.server}) - l.WithError(err).Debug("GoSMTP not serving") -} - -func monitorDisconnectedUsers(s Server, l listener.Listener, done <-chan void) { - ch := make(chan string) - l.Add(events.CloseConnectionEvent, ch) - for { - select { - case <-done: - return - case address := <-ch: - s.DisconnectUser(address) - } - } -} diff --git a/internal/serverutil/listener.go b/internal/serverutil/listener.go deleted file mode 100644 index 848f8d8c..00000000 --- a/internal/serverutil/listener.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package serverutil - -import ( - "io" - "net" - - "github.com/sirupsen/logrus" -) - -// connListener sets debug loggers on server containing fields with local -// and remote addresses right after new connection is accepted. -type connListener struct { - net.Listener - - server Server -} - -func (l *connListener) Accept() (net.Conn, error) { - conn, err := l.Listener.Accept() - - if err == nil && (l.server.DebugServer() || l.server.DebugClient()) { - debugLog := logrus.WithField("pkg", l.server.Protocol()) - if addr := conn.LocalAddr(); addr != nil { - debugLog = debugLog.WithField("loc", addr.String()) - } - if addr := conn.RemoteAddr(); addr != nil { - debugLog = debugLog.WithField("rem", addr.String()) - } - - var localDebug, remoteDebug io.Writer - if l.server.DebugServer() { - localDebug = debugLog.WithField("comm", "server").WriterLevel(logrus.DebugLevel) - } - if l.server.DebugClient() { - remoteDebug = debugLog.WithField("comm", "client").WriterLevel(logrus.DebugLevel) - } - - l.server.SetLoggers(localDebug, remoteDebug) - } - - return conn, err -} diff --git a/internal/serverutil/protocol.go b/internal/serverutil/protocol.go deleted file mode 100644 index c14d2f60..00000000 --- a/internal/serverutil/protocol.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package serverutil - -type Protocol string - -const ( - HTTP = Protocol("HTTP") - IMAP = Protocol("IMAP") - SMTP = Protocol("SMTP") -) diff --git a/internal/serverutil/server.go b/internal/serverutil/server.go deleted file mode 100644 index 2f5f4a1b..00000000 --- a/internal/serverutil/server.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package serverutil - -import ( - "crypto/tls" - "io" - "net" -) - -// Server can handle disconnected users. -type Server interface { - Protocol() Protocol - UseSSL() bool - Address() string - TLSConfig() *tls.Config - - DebugServer() bool - DebugClient() bool - SetLoggers(localDebug, remoteDebug io.Writer) - - HandlePanic() - DisconnectUser(string) - Serve(net.Listener) error - StopServe() error -} diff --git a/internal/serverutil/test/controller_test.go b/internal/serverutil/test/controller_test.go deleted file mode 100644 index 22986fca..00000000 --- a/internal/serverutil/test/controller_test.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package test - -import ( - "net/http" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/serverutil" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/stretchr/testify/require" -) - -func setup(t *testing.T) (*require.Assertions, *testServer, listener.Listener, serverutil.Controller) { - r := require.New(t) - s := newTestServer() - l := listener.New() - c := serverutil.NewController(s, l) - - return r, s, l, c -} - -func TestControllerListernServeClose(t *testing.T) { - r, s, l, c := setup(t) - - errorCh := l.ProvideChannel(events.ErrorEvent) - - r.True(s.portIsFree()) - go c.ListenAndServe() - r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) - - r.NoError(s.ping()) - - r.Nil(s.localDebug) - r.Nil(s.remoteDebug) - - c.Close() - r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) - - select { - case msg := <-errorCh: - r.Fail("Expected no error but have %q", msg) - case <-time.Tick(100 * time.Millisecond): - break - } -} - -func TestControllerFailOnBusyPort(t *testing.T) { - r, s, l, c := setup(t) - - ocupator := http.Server{Addr: s.Address()} - defer ocupator.Close() //nolint:errcheck - - go ocupator.ListenAndServe() //nolint:errcheck - r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) - - errorCh := l.ProvideChannel(events.ErrorEvent) - go c.ListenAndServe() - - r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) - - select { - case <-errorCh: - break - case <-time.Tick(time.Second): - r.Fail("Expected error but have none.") - } -} - -func TestControllerCallDisconnectUser(t *testing.T) { - r, s, l, c := setup(t) - - go c.ListenAndServe() - r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) - r.NoError(s.ping()) - - l.Emit(events.CloseConnectionEvent, "") - r.Eventually(func() bool { return s.calledDisconnected == 1 }, time.Second, 50*time.Millisecond) - - c.Close() - r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) - - l.Emit(events.CloseConnectionEvent, "") - r.Equal(1, s.calledDisconnected) -} - -func TestDebugClient(t *testing.T) { - r, s, _, c := setup(t) - - s.debugServer = false - s.debugClient = true - - go c.ListenAndServe() - r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) - r.NoError(s.ping()) - - r.Nil(s.localDebug) - r.NotNil(s.remoteDebug) - - c.Close() - r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) -} - -func TestDebugServer(t *testing.T) { - r, s, _, c := setup(t) - - s.debugServer = true - s.debugClient = false - - go c.ListenAndServe() - r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) - r.NoError(s.ping()) - - r.NotNil(s.localDebug) - r.Nil(s.remoteDebug) - - c.Close() - r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) -} - -func TestDebugBoth(t *testing.T) { - r, s, _, c := setup(t) - - s.debugServer = true - s.debugClient = true - - go c.ListenAndServe() - r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond) - r.NoError(s.ping()) - - r.NotNil(s.localDebug) - r.NotNil(s.remoteDebug) - - c.Close() - r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond) -} diff --git a/internal/serverutil/test/server.go b/internal/serverutil/test/server.go deleted file mode 100644 index 246dc57b..00000000 --- a/internal/serverutil/test/server.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package test - -import ( - "crypto/tls" - "fmt" - "io" - "net" - "net/http" - - "github.com/ProtonMail/proton-bridge/v2/internal/serverutil" - "github.com/ProtonMail/proton-bridge/v2/pkg/ports" -) - -func newTestServer() *testServer { - return &testServer{port: 11188} -} - -type testServer struct { - http http.Server - - useSSL, - debugServer, - debugClient bool - calledDisconnected int - - port int - tls *tls.Config - - localDebug, remoteDebug io.Writer -} - -func (*testServer) Protocol() serverutil.Protocol { return serverutil.HTTP } -func (s *testServer) UseSSL() bool { return s.useSSL } -func (s *testServer) Address() string { return fmt.Sprintf("127.0.0.1:%d", s.port) } -func (s *testServer) TLSConfig() *tls.Config { return s.tls } -func (s *testServer) HandlePanic() {} - -func (s *testServer) DebugServer() bool { return s.debugServer } -func (s *testServer) DebugClient() bool { return s.debugClient } -func (s *testServer) SetLoggers(localDebug, remoteDebug io.Writer) { - s.localDebug = localDebug - s.remoteDebug = remoteDebug -} - -func (s *testServer) DisconnectUser(string) { - s.calledDisconnected++ -} - -func (s *testServer) Serve(l net.Listener) error { - return s.http.Serve(l) -} - -func (s *testServer) StopServe() error { return s.http.Close() } - -func (s *testServer) portIsFree() bool { - return ports.IsPortFree(s.port) -} - -func (s *testServer) portIsOccupied() bool { - return !ports.IsPortFree(s.port) -} - -func (s *testServer) ping() error { - client := &http.Client{} - resp, err := client.Get("http://" + s.Address() + "/ping") - if err != nil { - return err - } - - return resp.Body.Close() -} diff --git a/internal/smtp/backend.go b/internal/smtp/backend.go deleted file mode 100644 index ffedb9f0..00000000 --- a/internal/smtp/backend.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "strings" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/internal/users" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - goSMTPBackend "github.com/emersion/go-smtp" - "github.com/pkg/errors" -) - -type panicHandler interface { - HandlePanic() -} - -type settingsProvider interface { - GetBool(settings.Key) bool -} - -type smtpBackend struct { - panicHandler panicHandler - eventListener listener.Listener - settings settingsProvider - bridge bridger - sendRecorder *sendRecorder -} - -// NewSMTPBackend returns struct implementing go-smtp/backend interface. -func NewSMTPBackend( - panicHandler panicHandler, - eventListener listener.Listener, - settings settingsProvider, - bridge *bridge.Bridge, -) *smtpBackend { //nolint:revive - return newSMTPBackend(panicHandler, eventListener, settings, newBridgeWrap(bridge)) -} - -func newSMTPBackend( - panicHandler panicHandler, - eventListener listener.Listener, - settings settingsProvider, - bridge bridger, -) *smtpBackend { - return &smtpBackend{ - panicHandler: panicHandler, - eventListener: eventListener, - settings: settings, - bridge: bridge, - sendRecorder: newSendRecorder(), - } -} - -// Login authenticates a user. -func (sb *smtpBackend) Login(_ *goSMTPBackend.ConnectionState, username, password string) (goSMTPBackend.Session, error) { - // Called from go-smtp in goroutines - we need to handle panics for each function. - defer sb.panicHandler.HandlePanic() - - if sb.bridge.HasError(bridge.ErrLocalCacheUnavailable) { - return nil, users.ErrLoggedOutUser - } - - username = strings.ToLower(username) - - user, err := sb.bridge.GetUser(username) - if err != nil { - log.Warn("Cannot get user: ", err) - return nil, err - } - if err := user.CheckBridgeLogin(password); err != nil { - log.WithError(err).Error("Could not check bridge password") - // Apple Mail sometimes generates a lot of requests very quickly. It's good practice - // to have a timeout after bad logins so that we can slow those requests down a little bit. - time.Sleep(10 * time.Second) - return nil, err - } - // Client can log in only using address so we can properly close all SMTP connections. - addressID, err := user.GetAddressID(username) - if err != nil { - log.Error("Cannot get addressID: ", err) - return nil, err - } - // AddressID is only for split mode--it has to be empty for combined mode. - if user.IsCombinedAddressMode() { - addressID = "" - } - return newSMTPUser(sb.panicHandler, sb.eventListener, sb, user, username, addressID) -} - -func (sb *smtpBackend) AnonymousLogin(_ *goSMTPBackend.ConnectionState) (goSMTPBackend.Session, error) { - // Called from go-smtp in goroutines - we need to handle panics for each function. - defer sb.panicHandler.HandlePanic() - - return nil, errors.New("anonymous login not supported") -} diff --git a/internal/smtp/bridge.go b/internal/smtp/bridge.go deleted file mode 100644 index f3845392..00000000 --- a/internal/smtp/bridge.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/users" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -type bridger interface { - GetUser(query string) (bridgeUser, error) - HasError(err error) bool -} - -type bridgeUser interface { - CheckBridgeLogin(password string) error - IsCombinedAddressMode() bool - GetAddressID(address string) (string, error) - GetClient() pmapi.Client - GetStore() storeUserProvider -} - -type bridgeWrap struct { - *bridge.Bridge -} - -// newBridgeWrap wraps bridge struct into local bridgeWrap to implement local -// interface. The problem is that bridge returns package bridge's User type, so -// every method that returns User has to be overridden to fulfill the interface. -func newBridgeWrap(bridge *bridge.Bridge) *bridgeWrap { - return &bridgeWrap{Bridge: bridge} -} - -func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) { - user, err := b.Bridge.GetUser(query) - if err != nil { - return nil, err - } - return newBridgeUserWrap(user), nil //nolint:typecheck missing methods are inherited -} - -type bridgeUserWrap struct { - *users.User -} - -func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap { - return &bridgeUserWrap{User: bridgeUser} -} - -func (u *bridgeUserWrap) GetStore() storeUserProvider { - // We need to explicitly return nil otherwise it's wrapped nil - // and condition `store == nil` would fail. - store := u.User.GetStore() - if store == nil { - return nil - } - return store -} diff --git a/internal/smtp/dump_default.go b/internal/smtp/dump_default.go deleted file mode 100644 index 8452bf94..00000000 --- a/internal/smtp/dump_default.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build !build_qa -// +build !build_qa - -package smtp - -func dumpMessageData([]byte, string) {} diff --git a/internal/smtp/dump_qa.go b/internal/smtp/dump_qa.go deleted file mode 100644 index 6af16fe6..00000000 --- a/internal/smtp/dump_qa.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build build_qa -// +build build_qa - -package smtp - -import ( - "fmt" - "os" - "path/filepath" - "time" - - "github.com/sirupsen/logrus" -) - -func dumpMessageData(b []byte, subject string) { - home, err := os.UserHomeDir() - if err != nil { - logrus.WithError(err).Error("Failed to dump raw message data") - return - } - - path := filepath.Join(home, "bridge-qa") - - if err := os.MkdirAll(path, 0o700); err != nil { - logrus.WithError(err).Error("Failed to dump raw message data") - return - } - - if len(subject) > 16 { - subject = subject[:16] - } - - if err := os.WriteFile( - filepath.Join(path, fmt.Sprintf("%v-%v.eml", subject, time.Now().Unix())), - b, - 0o600, - ); err != nil { - logrus.WithError(err).Error("Failed to dump raw message data") - return - } - - logrus.WithField("path", path).Info("Dumped raw message data") -} diff --git a/internal/smtp/keys_test.go b/internal/smtp/keys_test.go deleted file mode 100644 index 1bff99a8..00000000 --- a/internal/smtp/keys_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -const testPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- - -xsBNBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefEWSHl -CjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39vPiLJXUq -Zs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKiMeVa+GLEHhgZ -2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5c8CmpqJuASIJNrSX -M/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrbDEVRA2/BCJonw7aASiNC -rSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEBAAHNBlVzZXJJRMLAcgQQAQgA -JgUCVEltzwYLCQgHAwIJED62JZ7fId8kBBUIAgoDFgIBAhsDAh4BAAD0nQf9EtH9 -TC0JqSs8q194Zo244jjlJFM3EzxOSULq0zbywlLORfyoo/O8jU/HIuGz+LT98JDt -nltTqfjWgu6pS3ZL2/L4AGUKEoB7OI6oIdRwzMc61sqI+Qpbzxo7rzufH4CiXZc6 -cxORUgL550xSCcqnq0q1mds7h5roKDzxMW6WLiEsc1dN8IQKzC7Ec5wA7U4oNGsJ -3TyI8jkIs0IhXrRCd26K0TW8Xp6GCsfblWXosR13y89WVNgC+xrrJKTZEisc0tRl -neIgjcwEUvwfIg2n9cDUFA/5BsfzTW5IurxqDEziIVP0L44PXjtJrBQaGMPlEbtP -5i2oi3OADVX2XbvsRc7ATQRUSW3PAQgAkPnu5fps5zhOB/e618v/iF3KiogxUeRh -A68TbvA+xnFfTxCx2Vo14aOL0CnaJ8gO5yRSqfomL2O1kMq07N1MGbqucbmc+aSf -oElc+Gd5xBE/w3RcEhKcAaYTi35vG22zlZup4x3ElioyIarOssFEkQgNNyDf5AXZ -jdHLA6qVxeqAb/Ff74+y9HUmLPSsRU9NwFzvK3Jv8C/ubHVLzTYdFgYkc4W1Uug9 -Ou08K+/4NEMrwnPFBbZdJAuUjQz2zW2ZiEKiBggiorH2o5N3mYUnWEmUvqL3EOS8 -TbWo8UBIW3DDm2JiZR8VrEgvBtc9mVDUj/x+5pR07Fy1D6DjRmAc9wARAQABwsBf -BBgBCAATBQJUSW3SCRA+tiWe3yHfJAIbDAAA/iwH/ik9RKZMB9Ir0x5mGpKPuqhu -gwrc3d04m1sOdXJm2NtD4ddzSEvzHwaPNvEvUl5v7FVMzf6+6mYGWHyNP4+e7Rtw -YLlRpud6smuGyDSsotUYyumiqP6680ZIeWVQ+a1TThNs878mAJy1FhvQFdTmA8XI -C616hDFpamQKPlpoO1a0wZnQhrPwT77HDYEEa+hqY4Jr/a7ui40S+7xYRHKL/7ZA -S4/grWllhU3dbNrwSzrOKwrA/U0/9t738Ap6JL71YymDeaL4sutcoaahda1pTrMW -ePtrCltz6uySwbZs7GXoEzjX3EAH+6qhkUJtzMaE3YEFEoQMGzcDTUEfXCJ3zJw= -=yT9U ------END PGP PUBLIC KEY BLOCK-----` - -const testOtherPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBF8Rmj4BCACgXXxRqLsmEUWZGd0f88BteXBfi9zL+9GysOTk4n9EgINLN2PU -5rYSmWvVocO8IAfl/z9zpTJQesQjGe5lHbygUWFmjadox2ZeecZw0PWCSRdAjk6w -Q4UX0JiCo3IuICZk1t53WWRtGnhA2Q21J4b2DJg4T5ZFKgKDzDhWoGF1ZStbI5X1 -0rKTGFNHgreV5PqxUjxHVtx3rgT9Mx+13QTffqKR9oaYC6mNs4TNJdhyqfaYxqGw -ElxfdS9Wz6ODXrUNuSHETfgvAmo1Qep7GkefrC1isrmXA2+a+mXzFn4L0FCG073w -Vi/lEw6R/vKfN6QukHPxwoSguow4wTyhRRmfABEBAAG0GVRlc3RUZXN0IDx0ZXN0 -dGVzdEBwbS5tZT6JAU4EEwEIADgWIQTsXZU1AxlWCPT02+BKdWAu4Q1jXQUCXxGa -PgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBKdWAu4Q1jXQw+B/0ZudN+ -W9EqJtL/elm7Qla47zNsFmB+pHObdGoKtp3mNc97CQoW1yQ/i/V0heBFTAioP00g -FgEk1ZUJfO++EtI8esNFdDZqY99826/Cl0FlJwubn/XYxi4XyaGTY1nhhyEJ2HWI -/mZ+Jfm9ojbHSLwO5/AHiQt5t+LPDsKLXZw1BDJTgf1xD6e36CwAZgrPGWDqCXJ9 -BjlQn5hje7p0F8vYWBnnfSPkMHwibz9FlFqDh5v3XTgGpFIWDVkPVgAs8erM9AM2 -TjdpGcdW8xfcymo3j/o2QUBGYGJwPTsGEO5IkFRre9c/3REa7MKIi17Y479ub0A6 -2J3xgnqgI4sxmgmOuQENBF8Rmj4BCADX3BamNZsjC3I0knVIwjbz//1r8WOfNwGh -gg5LsvpfLkrsNUZy+deSwb+hS9Auyr1xsMmtVyiTPGUXTjU4uUzY2zyTYWgYfSEi -CojlXmYYLsjyPzR7KhVP6QIYZqYkOQXaCQDRlprRoFIEe4FzTCuqDHatJNwSesGy -5pPJrjiAeb47m9KaoEIacoe9D3w1z4FCKN3A8cjiWT8NRfhYTBoE/T34oXVUj8l+ -jLIgVUQgGoBos160Z1Cnxd2PKWFVh/Br3QtIPTbNVDWhh5T1+N2ypbwsXCawy6fj -cbOaTLz/vF9g+RJKC0MtxdL5qUtv3d3Zn07Sg+9H6wjsboAdAvirABEBAAGJATYE -GAEIACAWIQTsXZU1AxlWCPT02+BKdWAu4Q1jXQUCXxGaPgIbDAAKCRBKdWAu4Q1j -Xc4WB/9+aTGMMTlIdAFs9rf0i7i83pUOOxuLl34YQ0t5WGsjteQ4IK+gfuFvp37W -ktv98ShOxAexbfqzGyGcYLLgaCxCbbB85fvSeX0xK/C2UbiH3Gv1z8GTelailCxt -vyx642TwpcLXW1obHaHTSIi5L35Tce9gbug9sKCRSlAH76dANYBbMLa2Bl0LSrF8 -mcie9jJaPRXGOeHOyZmPZwwGhVYgadjptWqXnFz3ua8vxgqG0sefWF23F36iVz2q -UjxSE+nKLaPFLlEDLgxG4SwHkcR9fi7zaQVnXg4rEjr0uz5MSUqZC4MNB4rkhU3g -/rUMQyZupw+xJ+ayQNVBEtYZd/9u -=TNX4 ------END PGP PUBLIC KEY BLOCK-----` diff --git a/internal/smtp/preferences_test.go b/internal/smtp/preferences_test.go deleted file mode 100644 index 4e880521..00000000 --- a/internal/smtp/preferences_test.go +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPreferencesBuilder(t *testing.T) { - testContactKey := loadContactKey(t, testPublicKey) - testOtherContactKey := loadContactKey(t, testOtherPublicKey) - - tests := []struct { //nolint:maligned - name string - - contactMeta *ContactMetadata - receivedKeys []pmapi.PublicKey - isInternal bool - mailSettings pmapi.MailSettings - composerMIMEType string - - wantEncrypt bool - wantSign bool - wantScheme pmapi.PackageFlag - wantMIMEType string - wantPublicKey string - }{ - { - name: "internal", - - contactMeta: &ContactMetadata{}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: true, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.InternalPackage, - wantMIMEType: "text/html", - wantPublicKey: testPublicKey, - }, - - { - name: "internal with contact-specific email format", - - contactMeta: &ContactMetadata{MIMEType: "text/plain"}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: true, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.InternalPackage, - wantMIMEType: "text/plain", - wantPublicKey: testPublicKey, - }, - - { - name: "internal with pinned contact public key", - - contactMeta: &ContactMetadata{Keys: []string{testContactKey}}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: true, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.InternalPackage, - wantMIMEType: "text/html", - wantPublicKey: testPublicKey, - }, - - { - // NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation. - name: "internal with conflicting contact public key", - - contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: true, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.InternalPackage, - wantMIMEType: "text/html", - wantPublicKey: testPublicKey, - }, - - { - name: "wkd-external", - - contactMeta: &ContactMetadata{}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPMIMEPackage, - wantMIMEType: "multipart/mixed", - wantPublicKey: testPublicKey, - }, - - { - name: "wkd-external with contact-specific email format", - - contactMeta: &ContactMetadata{MIMEType: "text/plain"}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPMIMEPackage, - wantMIMEType: "multipart/mixed", - wantPublicKey: testPublicKey, - }, - - { - name: "wkd-external with global pgp-inline scheme", - - contactMeta: &ContactMetadata{}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPInlinePackage, - wantMIMEType: "text/plain", - wantPublicKey: testPublicKey, - }, - - { - name: "wkd-external with contact-specific pgp-inline scheme overriding global pgp-mime setting", - - contactMeta: &ContactMetadata{Scheme: pgpInline}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPInlinePackage, - wantMIMEType: "text/plain", - wantPublicKey: testPublicKey, - }, - - { - name: "wkd-external with contact-specific pgp-mime scheme overriding global pgp-inline setting", - - contactMeta: &ContactMetadata{Scheme: pgpMIME}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPMIMEPackage, - wantMIMEType: "multipart/mixed", - wantPublicKey: testPublicKey, - }, - - { - name: "wkd-external with additional pinned contact public key", - - contactMeta: &ContactMetadata{Keys: []string{testContactKey}}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPMIMEPackage, - wantMIMEType: "multipart/mixed", - wantPublicKey: testPublicKey, - }, - - { - // NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation. - name: "wkd-external with additional conflicting contact public key", - - contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}}, - receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPMIMEPackage, - wantMIMEType: "multipart/mixed", - wantPublicKey: testPublicKey, - }, - - { - name: "external", - - contactMeta: &ContactMetadata{}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: false, - wantSign: false, - wantScheme: pmapi.ClearPackage, - wantMIMEType: "text/html", - }, - - { - name: "external with contact-specific email format", - - contactMeta: &ContactMetadata{MIMEType: "text/plain"}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: false, - wantSign: false, - wantScheme: pmapi.ClearPackage, - wantMIMEType: "text/plain", - }, - - { - name: "external with sign enabled", - - contactMeta: &ContactMetadata{Sign: true, SignIsSet: true}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: false, - wantSign: true, - wantScheme: pmapi.ClearMIMEPackage, - wantMIMEType: "multipart/mixed", - }, - - { - name: "external with contact sign enabled and plain text", - - contactMeta: &ContactMetadata{MIMEType: "text/plain", Scheme: pgpInline, Sign: true, SignIsSet: true}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: false, - wantSign: true, - wantScheme: pmapi.ClearPackage, - wantMIMEType: "text/plain", - }, - - { - name: "external with sign enabled, sending plaintext, should still send as ClearMIME", - - contactMeta: &ContactMetadata{Sign: true, SignIsSet: true}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/plain"}, - - wantEncrypt: false, - wantSign: true, - wantScheme: pmapi.ClearMIMEPackage, - wantMIMEType: "multipart/mixed", - }, - - { - name: "external with pinned contact public key but no intention to encrypt/sign", - - contactMeta: &ContactMetadata{Keys: []string{testContactKey}}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: false, - wantSign: false, - wantScheme: pmapi.ClearPackage, - wantMIMEType: "text/html", - wantPublicKey: testPublicKey, - }, - - { - name: "external with pinned contact public key, encrypted and signed", - - contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPMIMEPackage, - wantMIMEType: "multipart/mixed", - wantPublicKey: testPublicKey, - }, - - { - name: "external with pinned contact public key, encrypted and signed using contact-specific pgp-inline", - - contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, Scheme: pgpInline, SignIsSet: true}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPInlinePackage, - wantMIMEType: "text/plain", - wantPublicKey: testPublicKey, - }, - - { - name: "external with pinned contact public key, encrypted and signed using global pgp-inline", - - contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true}, - receivedKeys: []pmapi.PublicKey{}, - isInternal: false, - mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"}, - - wantEncrypt: true, - wantSign: true, - wantScheme: pmapi.PGPInlinePackage, - wantMIMEType: "text/plain", - wantPublicKey: testPublicKey, - }, - } - - for _, test := range tests { - test := test // Avoid using range scope test inside function literal. - - t.Run(test.name, func(t *testing.T) { - b := &sendPreferencesBuilder{} - - require.NoError(t, b.setPGPSettings(test.contactMeta, test.receivedKeys, test.isInternal)) - b.setEncryptionPreferences(test.mailSettings) - b.setMIMEPreferences(test.composerMIMEType) - - prefs := b.build() - - assert.Equal(t, test.wantEncrypt, prefs.Encrypt) - assert.Equal(t, test.wantSign, prefs.Sign) - assert.Equal(t, test.wantScheme, prefs.Scheme) - assert.Equal(t, test.wantMIMEType, prefs.MIMEType) - - if prefs.PublicKey != nil { - wantKey, err := crypto.NewKeyFromArmored(test.wantPublicKey) - require.NoError(t, err) - - haveKey, err := prefs.PublicKey.GetKey(0) - require.NoError(t, err) - - assert.Equal(t, wantKey.GetFingerprint(), haveKey.GetFingerprint()) - } - }) - } -} - -func loadContactKey(t *testing.T, key string) string { - ck, err := crypto.NewKeyFromArmored(key) - require.NoError(t, err) - - pk, err := ck.GetPublicKey() - require.NoError(t, err) - - return string(pk) -} diff --git a/internal/smtp/repro_test.go b/internal/smtp/repro_test.go deleted file mode 100644 index 796f0a20..00000000 --- a/internal/smtp/repro_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/assert" -) - -func TestKeyRingsAreEqualAfterFiltering(t *testing.T) { - // Load the key. - key, err := crypto.NewKeyFromArmored(testPublicKey) - if err != nil { - panic(err) - } - - // Put it in a keyring. - keyRing, err := crypto.NewKeyRing(key) - if err != nil { - panic(err) - } - - // Filter out expired ones. - validKeyRings, err := crypto.FilterExpiredKeys([]*crypto.KeyRing{keyRing}) - if err != nil { - panic(err) - } - - // Filtering shouldn't make them unequal. - assert.True(t, isEqual(t, keyRing, validKeyRings[0])) -} - -func isEqual(t *testing.T, a, b *crypto.KeyRing) bool { - if a == nil && b == nil { - return true - } - - if a == nil && b != nil || a != nil && b == nil { - return false - } - - aKeys, bKeys := a.GetKeys(), b.GetKeys() - - if len(aKeys) != len(bKeys) { - return false - } - - for i := range aKeys { - aFPs := aKeys[i].GetSHA256Fingerprints() - bFPs := bKeys[i].GetSHA256Fingerprints() - - if !assert.Equal(t, aFPs, bFPs) { - return false - } - } - - return true -} diff --git a/internal/smtp/send_recorder.go b/internal/smtp/send_recorder.go deleted file mode 100644 index a7e6e37a..00000000 --- a/internal/smtp/send_recorder.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "context" - "crypto/sha256" - "fmt" - "strings" - "sync" - "time" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -type messageGetter interface { - GetMessage(context.Context, string) (*pmapi.Message, error) -} - -type sendRecorderValue struct { - messageID string - time time.Time -} - -type sendRecorder struct { - lock *sync.RWMutex - hashes map[string]sendRecorderValue -} - -func newSendRecorder() *sendRecorder { - return &sendRecorder{ - lock: &sync.RWMutex{}, - hashes: map[string]sendRecorderValue{}, - } -} - -func (q *sendRecorder) getMessageHash(message *pmapi.Message) string { - // Outlook Calendar updates has only headers (no body) and thus have always - // the same hash. If the message is type of calendar, the "is sending" - // check to avoid potential duplicates is skipped. Duplicates should not - // be a problem in this case as calendar updates are small. - contentType := message.Header.Get("Content-Type") - if strings.HasPrefix(contentType, "text/calendar") { - return "" - } - - h := sha256.New() - _, _ = h.Write([]byte(message.AddressID + message.Subject)) - if message.Sender != nil { - _, _ = h.Write([]byte(message.Sender.Address)) - } - for _, to := range message.ToList { - _, _ = h.Write([]byte(to.Address)) - } - for _, to := range message.CCList { - _, _ = h.Write([]byte(to.Address)) - } - for _, to := range message.BCCList { - _, _ = h.Write([]byte(to.Address)) - } - _, _ = h.Write([]byte(message.Body)) - for _, att := range message.Attachments { - _, _ = h.Write([]byte(att.Name + att.MIMEType + fmt.Sprintf("%d", att.Size))) - } - return fmt.Sprintf("%x", h.Sum(nil)) -} - -func (q *sendRecorder) addMessage(hash string) { - q.lock.Lock() - defer q.lock.Unlock() - - q.deleteExpiredKeys() - q.hashes[hash] = sendRecorderValue{ - time: time.Now(), - } -} - -func (q *sendRecorder) removeMessage(hash string) { - q.lock.Lock() - defer q.lock.Unlock() - - q.deleteExpiredKeys() - delete(q.hashes, hash) -} - -func (q *sendRecorder) setMessageID(hash, messageID string) { - q.lock.Lock() - defer q.lock.Unlock() - - if val, ok := q.hashes[hash]; ok { - val.messageID = messageID - q.hashes[hash] = val - } -} - -func (q *sendRecorder) isSendingOrSent(client messageGetter, hash string) (isSending bool, wasSent bool) { - q.lock.Lock() - defer q.lock.Unlock() - - if hash == "" { - return false, false - } - - q.deleteExpiredKeys() - value, ok := q.hashes[hash] - if !ok { - return - } - - // If we have a value but don't yet have a messageID, we are in the process of uploading the draft. - if value.messageID == "" { - return true, false - } - - message, err := client.GetMessage(context.TODO(), value.messageID) - // Message could be deleted or there could be an internet issue or whatever, - // so let's assume the message was not sent. - if err != nil { - return - } - if message.IsDraft() { - // If message is in draft for a long time, let's assume there is - // some problem and message will not be sent anymore. - if time.Since(time.Unix(message.Time, 0)).Minutes() > 10 { - return - } - isSending = true - } - // Message can be in Inbox and Sent when message was sent to myself. - if message.Has(pmapi.FlagSent) { - wasSent = true - } - - return isSending, wasSent -} - -func (q *sendRecorder) deleteExpiredKeys() { - for key, value := range q.hashes { - // It's hard to find a good expiration time. - // On the one hand, a user could set up some cron job sending the same message over and over again (heartbeat). - // On the other, a user could put the device into sleep mode while sending. - // Changing the expiration time will always make one of the edge cases worse. - // But both edge cases are something we don't care much about. Important thing is we don't send the same message many times. - if time.Since(value.time) > 30*time.Minute { - delete(q.hashes, key) - } - } -} diff --git a/internal/smtp/send_recorder_test.go b/internal/smtp/send_recorder_test.go deleted file mode 100644 index 375c7ea2..00000000 --- a/internal/smtp/send_recorder_test.go +++ /dev/null @@ -1,446 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "context" - "errors" - "fmt" - "net/mail" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/stretchr/testify/assert" -) - -type testSendRecorderGetMessageMock struct { - message *pmapi.Message - err error -} - -func (m *testSendRecorderGetMessageMock) GetMessage(_ context.Context, messageID string) (*pmapi.Message, error) { - return m.message, m.err -} - -func TestSendRecorder_getMessageHash(t *testing.T) { - q := newSendRecorder() - - message := &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - } - hash := q.getMessageHash(message) - - testCases := []struct { - message *pmapi.Message - expectEqual bool - }{ - { - message, - true, - }, - { - &pmapi.Message{}, - false, - }, - { // Different AddressID - &pmapi.Message{ - AddressID: "...", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different subject - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1.", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different sender - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "sender@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different ToList - changed address - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "other@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different ToList - more addresses - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - {Address: "another@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different CCList - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different BCCList - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different body - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body.", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different attachment - no attachment - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{}, - }, - false, - }, - { // Different attachment - name - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "...", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - { // Different attachment - MIMEType - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/jpeg", - Size: 12345, - }, - }, - }, - false, - }, - { // Different attachment - Size - &pmapi.Message{ - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 42, - }, - }, - }, - false, - }, - { // Different content type - calendar - &pmapi.Message{ - Header: mail.Header{ - "Content-Type": []string{"text/calendar"}, - }, - AddressID: "address123", - Subject: "Subject #1", - Sender: &mail.Address{ - Address: "from@pm.me", - }, - ToList: []*mail.Address{ - {Address: "to@pm.me"}, - }, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Body: "body", - Attachments: []*pmapi.Attachment{ - { - Name: "att1", - MIMEType: "image/png", - Size: 12345, - }, - }, - }, - false, - }, - } - for i, tc := range testCases { - tc := tc // bind - t.Run(fmt.Sprintf("%d / %v", i, tc.message), func(t *testing.T) { - newHash := q.getMessageHash(tc.message) - if tc.expectEqual { - assert.Equal(t, hash, newHash) - } else { - assert.NotEqual(t, hash, newHash) - } - }) - } -} - -func TestSendRecorder_isSendingOrSent(t *testing.T) { - q := newSendRecorder() - q.addMessage("hash") - q.setMessageID("hash", "messageID") - - draftFlag := pmapi.FlagInternal | pmapi.FlagE2E - selfSent := pmapi.FlagSent | pmapi.FlagReceived - - testCases := []struct { - hash string - message *pmapi.Message - err error - wantIsSending bool - wantWasSent bool - }{ - {"badhash", &pmapi.Message{Flags: draftFlag}, nil, false, false}, - {"hash", nil, errors.New("message not found"), false, false}, - {"hash", &pmapi.Message{Flags: pmapi.FlagReceived}, nil, false, false}, - {"hash", &pmapi.Message{Flags: draftFlag, Time: time.Now().Add(-20 * time.Minute).Unix()}, nil, false, false}, - {"hash", &pmapi.Message{Flags: draftFlag, Time: time.Now().Unix()}, nil, true, false}, - {"hash", &pmapi.Message{Flags: pmapi.FlagSent}, nil, false, true}, - {"hash", &pmapi.Message{Flags: selfSent}, nil, false, true}, - {"", &pmapi.Message{Flags: selfSent}, nil, false, false}, - } - for i, tc := range testCases { - tc := tc // bind - t.Run(fmt.Sprintf("%d / %v / %v / %v", i, tc.hash, tc.message, tc.err), func(t *testing.T) { - messageGetter := &testSendRecorderGetMessageMock{message: tc.message, err: tc.err} - isSending, wasSent := q.isSendingOrSent(messageGetter, tc.hash) - assert.Equal(t, tc.wantIsSending, isSending, "isSending does not match") - assert.Equal(t, tc.wantWasSent, wasSent, "wasSent does not match") - }) - } -} - -func TestSendRecorder_deleteExpiredKeys(t *testing.T) { - q := newSendRecorder() - - q.hashes["hash1"] = sendRecorderValue{ - messageID: "msg1", - time: time.Now(), - } - q.hashes["hash2"] = sendRecorderValue{ - messageID: "msg2", - time: time.Now().Add(-31 * time.Minute), - } - - q.deleteExpiredKeys() - - _, ok := q.hashes["hash1"] - assert.True(t, ok) - _, ok = q.hashes["hash2"] - assert.False(t, ok) -} diff --git a/internal/smtp/server.go b/internal/smtp/server.go deleted file mode 100644 index cb5485ed..00000000 --- a/internal/smtp/server.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "crypto/tls" - "fmt" - "io" - "net" - - "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/serverutil" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/emersion/go-sasl" - goSMTP "github.com/emersion/go-smtp" -) - -// Server is Bridge SMTP server implementation. -type Server struct { - panicHandler panicHandler - backend goSMTP.Backend - debug bool - useSSL bool - port int - tls *tls.Config - - server *goSMTP.Server - controller serverutil.Controller -} - -// NewSMTPServer returns an SMTP server configured with the given options. -func NewSMTPServer( - panicHandler panicHandler, - debug bool, port int, useSSL bool, - tls *tls.Config, - smtpBackend goSMTP.Backend, - eventListener listener.Listener, -) *Server { - server := &Server{ - panicHandler: panicHandler, - backend: smtpBackend, - debug: debug, - useSSL: useSSL, - port: port, - tls: tls, - } - - server.server = newGoSMTPServer(server) - server.controller = serverutil.NewController(server, eventListener) - return server -} - -func newGoSMTPServer(s *Server) *goSMTP.Server { - newSMTP := goSMTP.NewServer(s.backend) - newSMTP.Addr = s.Address() - newSMTP.TLSConfig = s.tls - newSMTP.Domain = bridge.Host - newSMTP.ErrorLog = serverutil.NewServerErrorLogger(serverutil.SMTP) - newSMTP.AllowInsecureAuth = true - newSMTP.MaxLineLength = 1 << 16 - - newSMTP.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server { - return sasl.NewLoginServer(func(address, password string) error { - user, err := conn.Server().Backend.Login(nil, address, password) - if err != nil { - return err - } - - conn.SetSession(user) - return nil - }) - }) - return newSMTP -} - -// ListenAndServe will run server and all monitors. -func (s *Server) ListenAndServe() { s.controller.ListenAndServe() } - -// Close turns off server and monitors. -func (s *Server) Close() { s.controller.Close() } - -// Implements servertutil.Server interface. - -func (Server) Protocol() serverutil.Protocol { return serverutil.SMTP } -func (s *Server) UseSSL() bool { return s.useSSL } -func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) } -func (s *Server) TLSConfig() *tls.Config { return s.tls } -func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() } - -func (s *Server) DebugServer() bool { return s.debug } -func (s *Server) DebugClient() bool { return s.debug } - -func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) { s.server.Debug = localDebug } - -func (s *Server) DisconnectUser(address string) { - log.Info("Disconnecting all open SMTP connections for ", address) - s.server.ForEachConn(func(conn *goSMTP.Conn) { - connUser := conn.Session() - if connUser != nil { - if err := conn.Close(); err != nil { - log.WithError(err).Error("Failed to close the connection") - } - } - }) -} - -func (s *Server) Serve(l net.Listener) error { return s.server.Serve(l) } -func (s *Server) StopServe() error { return s.server.Close() } diff --git a/internal/smtp/smtp.go b/internal/smtp/smtp.go deleted file mode 100644 index 5df423cb..00000000 --- a/internal/smtp/smtp.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package smtp provides SMTP server of the Bridge. -package smtp - -import "github.com/sirupsen/logrus" - -var log = logrus.WithField("pkg", "smtp") //nolint:gochecknoglobals diff --git a/internal/smtp/store.go b/internal/smtp/store.go deleted file mode 100644 index e1ae3917..00000000 --- a/internal/smtp/store.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "io" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -type storeUserProvider interface { - CreateDraft( - kr *crypto.KeyRing, - message *pmapi.Message, - attachmentReaders []io.Reader, - attachedPublicKey, - attachedPublicKeyName string, - parentID string) (*pmapi.Message, []*pmapi.Attachment, error) - SendMessage(messageID string, req *pmapi.SendMessageReq) error - GetMaxUpload() (int64, error) -} diff --git a/internal/smtp/user.go b/internal/smtp/user.go deleted file mode 100644 index c66ab766..00000000 --- a/internal/smtp/user.go +++ /dev/null @@ -1,497 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// NOTE: Comments in this file refer to a specification in a document called -// "Proton Mail Encryption logic". It will be referred to via abbreviation PMEL. - -package smtp - -import ( - "bytes" - "context" - "encoding/base64" - "fmt" - "io" - "net/mail" - "strings" - "time" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - pkgMsg "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/message/parser" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - goSMTPBackend "github.com/emersion/go-smtp" - "github.com/pkg/errors" -) - -type smtpUser struct { - panicHandler panicHandler - eventListener listener.Listener - backend *smtpBackend - user bridgeUser - storeUser storeUserProvider - username string - addressID string - - returnPath string - to []string -} - -// newSMTPUser returns struct implementing go-smtp/session interface. -func newSMTPUser( - panicHandler panicHandler, - eventListener listener.Listener, - smtpBackend *smtpBackend, - user bridgeUser, - username string, - addressID string, -) (goSMTPBackend.Session, error) { - storeUser := user.GetStore() - if storeUser == nil { - return nil, errors.New("user database is not initialized") - } - - return &smtpUser{ - panicHandler: panicHandler, - eventListener: eventListener, - backend: smtpBackend, - user: user, - storeUser: storeUser, - username: username, - addressID: addressID, - }, nil -} - -// This method should eventually no longer be necessary. Everything should go via store. -func (su *smtpUser) client() pmapi.Client { - return su.user.GetClient() -} - -// Send sends an email from the given address to the given addresses with the given body. -func (su *smtpUser) getSendPreferences( - recipient, messageMIMEType string, - mailSettings pmapi.MailSettings, -) (preferences SendPreferences, err error) { - b := &sendPreferencesBuilder{} - - // 1. contact vcard data - vCardData, err := su.getContactVCardData(recipient) - if err != nil { - return - } - - // 2. api key data - apiKeys, isInternal, err := su.getAPIKeyData(recipient) - if err != nil { - return - } - - // 1 + 2 -> 3. advanced PGP settings - if err = b.setPGPSettings(vCardData, apiKeys, isInternal); err != nil { - return - } - - // 4. mail settings - // Passed in from su.client().GetMailSettings() - - // 3 + 4 -> 5. encryption preferences - b.setEncryptionPreferences(mailSettings) - - // 6. composer preferences -- in our case, this comes from the MIME type of the message. - - // 5 + 6 -> 7. send preferences - b.setMIMEPreferences(messageMIMEType) - - return b.build(), nil -} - -func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata, err error) { - emails, err := su.client().GetContactEmailByEmail(context.TODO(), recipient, 0, 1000) - if err != nil { - return - } - - for _, email := range emails { - if email.Defaults == 1 { - // NOTE: Can we still ignore this? - continue - } - - var contact pmapi.Contact - if contact, err = su.client().GetContactByID(context.TODO(), email.ContactID); err != nil { - return - } - - var cards []pmapi.Card - if cards, err = su.client().DecryptAndVerifyCards(contact.Cards); err != nil { - return - } - - return GetContactMetadataFromVCards(cards, recipient) - } - - return -} - -func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey, isInternal bool, err error) { - return su.client().GetPublicKeysForEmail(context.TODO(), recipient) -} - -// Discard currently processed message. -func (su *smtpUser) Reset() { - log.Trace("Resetting the session") - su.returnPath = "" - su.to = []string{} -} - -// Set return path for currently processed message. -func (su *smtpUser) Mail(returnPath string, opts goSMTPBackend.MailOptions) error { - log.WithField("returnPath", returnPath).WithField("opts", opts).Trace("Setting mail from") - - // REQUIRETLS and SMTPUTF8 have to be announced to be used by client. - // Bridge does not use those extensions so this should not happen. - if opts.RequireTLS { - return errors.New("REQUIRETLS extension is not supported") - } - if opts.UTF8 { - return errors.New("SMTPUTF8 extension is not supported") - } - - if opts.Auth != nil && *opts.Auth != "" && *opts.Auth != su.username { - return errors.New("changing identity is not supported") - } - - if returnPath != "" { - addr := su.client().Addresses().ByEmail(returnPath) - if addr == nil { - return errors.New("backend: invalid return path: not owned by user") - } - } - - su.returnPath = returnPath - return nil -} - -// Add recipient for currently processed message. -func (su *smtpUser) Rcpt(to string) error { - log.WithField("to", to).Trace("Adding recipient") - if to != "" { - su.to = append(su.to, to) - } - return nil -} - -// Set currently processed message contents and send it. -func (su *smtpUser) Data(r io.Reader) error { - log.Trace("Sending the message") - if su.returnPath == "" { - return errors.New("missing return path") - } - if len(su.to) == 0 { - return errors.New("missing recipient") - } - return su.Send(su.returnPath, su.to, r) -} - -// Send sends an email from the given address to the given addresses with the given body. -func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader) (err error) { //nolint:funlen,gocyclo - // Called from go-smtp in goroutines - we need to handle panics for each function. - defer su.panicHandler.HandlePanic() - - b := new(bytes.Buffer) - - messageReader = io.TeeReader(messageReader, b) - - mailSettings, err := su.client().GetMailSettings(context.TODO()) - if err != nil { - return err - } - - returnPathAddr := su.client().Addresses().ByEmail(returnPath) - if returnPathAddr == nil { - err = errors.New("backend: invalid return path: not owned by user") - return - } - - parser, err := parser.New(messageReader) - if err != nil { - err = errors.Wrap(err, "failed to create new parser") - return - } - message, plainBody, attReaders, err := pkgMsg.ParserWithParser(parser) - if err != nil { - log.WithError(err).Error("Failed to parse message") - return - } - richBody := message.Body - - externalID := message.Header.Get("Message-Id") - externalID = strings.Trim(externalID, "<>") - - draftID, parentID := su.handleReferencesHeader(message) - - if err = su.handleSenderAndRecipients(message, returnPathAddr, returnPath, to); err != nil { - return err - } - - addr := su.client().Addresses().ByEmail(message.Sender.Address) - if addr == nil { - err = errors.New("backend: invalid email address: not owned by user") - return - } - - message.Sender.Address = pmapi.ConstructAddress(message.Sender.Address, addr.Email) - - kr, err := su.client().KeyRingForAddressID(addr.ID) - if err != nil { - return - } - - var attachedPublicKey string - var attachedPublicKeyName string - if mailSettings.AttachPublicKey > 0 { - firstKey, err := kr.GetKey(0) - if err != nil { - return err - } - - attachedPublicKey, err = firstKey.GetArmoredPublicKey() - if err != nil { - return err - } - - attachedPublicKeyName = fmt.Sprintf("publickey - %v - %v", kr.GetIdentities()[0].Name, firstKey.GetFingerprint()[:8]) - } - - if attachedPublicKey != "" { - pkgMsg.AttachPublicKey(parser, attachedPublicKey, attachedPublicKeyName) - } - - mimeBody, err := pkgMsg.BuildMIMEBody(parser) - if err != nil { - log.WithError(err).Error("Failed to build message") - return - } - - message.AddressID = addr.ID - - // Apple Mail Message-Id has to be stored to avoid recovered message after each send. - // Before it was done only for Apple Mail, but it should work for any client. Also, the client - // is set up from IMAP and no one can be sure that the same client is used for SMTP as well. - // Also, user can use more than one client which could break the condition as well. - // If there is any problem, condition to Apple Mail only should be returned. - // Note: for that, we would need to refactor a little bit and pass the last client name from - // the IMAP through the bridge user. - message.ExternalID = externalID - - // If Outlook does not get a response quickly, it will try to send the message again, leading - // to sending the same message multiple times. In case we detect the same message is in the - // sending queue, we wait a minute to finish the first request. If the message is still being - // sent after the timeout, we return an error back to the client. The UX is not the best, - // but it's better than sending the message many times. If the message was sent, we simply return - // nil to indicate it's OK. - sendRecorderMessageHash := su.backend.sendRecorder.getMessageHash(message) - isSending, wasSent := su.backend.sendRecorder.isSendingOrSent(su.client(), sendRecorderMessageHash) - - startTime := time.Now() - for isSending && time.Since(startTime) < 90*time.Second { - log.Warn("Message is still in send queue, waiting for a bit") - time.Sleep(15 * time.Second) - isSending, wasSent = su.backend.sendRecorder.isSendingOrSent(su.client(), sendRecorderMessageHash) - } - if isSending { - log.Warn("Message is still in send queue, returning error to prevent client from adding it to the sent folder prematurely") - return errors.New("original message is still being sent") - } - if wasSent { - log.Warn("Message was already sent") - return nil - } - - su.backend.sendRecorder.addMessage(sendRecorderMessageHash) - message, atts, err := su.storeUser.CreateDraft(kr, message, attReaders, attachedPublicKey, attachedPublicKeyName, parentID) - if err != nil { - su.backend.sendRecorder.removeMessage(sendRecorderMessageHash) - log.WithError(err).Error("Draft could not be created") - return err - } - su.backend.sendRecorder.setMessageID(sendRecorderMessageHash, message.ID) - log.WithField("messageID", message.ID).Debug("Draft was created successfully") - - // We always have to create a new draft even if there already is one, - // because clients don't necessarily save the draft before sending, which - // can lead to sending the wrong message. Also clients do not necessarily - // delete the old draft. - if draftID != "" { - if err := su.client().DeleteMessages(context.TODO(), []string{draftID}); err != nil { - log.WithError(err).WithField("draftID", draftID).Warn("Original draft cannot be deleted") - } - } - - atts = append(atts, message.Attachments...) - // Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys. - attkeys := make(map[string]*crypto.SessionKey) - - for _, att := range atts { - var keyPackets []byte - if keyPackets, err = base64.StdEncoding.DecodeString(att.KeyPackets); err != nil { - return errors.Wrap(err, "decoding attachment key packets") - } - if attkeys[att.ID], err = kr.DecryptSessionKey(keyPackets); err != nil { - return errors.Wrap(err, "decrypting attachment session key") - } - } - - req := pmapi.NewSendMessageReq(kr, mimeBody, plainBody, richBody, attkeys) - - for _, recipient := range message.Recipients() { - email := recipient.Address - if !looksLikeEmail(email) { - return errors.New(`"` + email + `" is not a valid recipient.`) - } - - sendPreferences, err := su.getSendPreferences(email, message.MIMEType, mailSettings) - if err != nil { - return err - } - - var signature pmapi.SignatureFlag - if sendPreferences.Sign { - signature = pmapi.SignatureDetached - } else { - signature = pmapi.SignatureNone - } - - if err := req.AddRecipient(email, sendPreferences.Scheme, sendPreferences.PublicKey, signature, sendPreferences.MIMEType, sendPreferences.Encrypt); err != nil { - return errors.Wrap(err, "failed to add recipient") - } - } - - req.PreparePackages() - - dumpMessageData(b.Bytes(), message.Subject) - - return su.storeUser.SendMessage(message.ID, req) -} - -func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID string) { - // Remove the internal IDs from the references header before sending to avoid confusion. - references := m.Header.Get("References") - newReferences := []string{} - for _, reference := range strings.Fields(references) { - if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) { - newReferences = append(newReferences, reference) - } else { // internalid is the parentID. - idMatch := pmapi.RxInternalReferenceFormat.FindStringSubmatch(reference) - if len(idMatch) == 2 { - lastID := idMatch[1] - filter := &pmapi.MessagesFilter{ID: []string{lastID}} - if su.addressID != "" { - filter.AddressID = su.addressID - } - metadata, _, _ := su.client().ListMessages(context.TODO(), filter) - for _, m := range metadata { - if m.IsDraft() { - draftID = m.ID - } else { - parentID = m.ID - } - } - } - } - } - - m.Header["References"] = newReferences - - if parentID == "" && len(newReferences) > 0 { - externalID := strings.Trim(newReferences[len(newReferences)-1], "<>") - filter := &pmapi.MessagesFilter{ExternalID: externalID} - if su.addressID != "" { - filter.AddressID = su.addressID - } - metadata, _, _ := su.client().ListMessages(context.TODO(), filter) - // There can be two or messages with the same external ID and then we cannot - // be sure which message should be parent. Better to not choose any. - if len(metadata) == 1 { - parentID = metadata[0].ID - } - } - - return draftID, parentID -} - -func (su *smtpUser) handleSenderAndRecipients(m *pmapi.Message, returnPathAddr *pmapi.Address, returnPath string, to []string) (err error) { - returnPath = pmapi.ConstructAddress(returnPath, returnPathAddr.Email) - - // Check sender. - if m.Sender == nil { - m.Sender = &mail.Address{Address: returnPath} - } else if m.Sender.Address == "" { - m.Sender.Address = returnPath - } - - // Check recipients. - if len(to) == 0 { - err = errors.New("backend: no recipient specified") - return - } - - // Sanitize ToList because some clients add *Sender* in the *ToList* when only Bcc is filled. - i := 0 - for _, keep := range m.ToList { - keepThis := false - for _, addr := range to { - if addr == keep.Address { - keepThis = true - break - } - } - if keepThis { - m.ToList[i] = keep - i++ - } - } - m.ToList = m.ToList[:i] - - // Build a map of recipients visible to all. - // Bcc should be empty when sending a message. - var recipients []*mail.Address - recipients = append(recipients, m.ToList...) - recipients = append(recipients, m.CCList...) - recipients = append(recipients, m.BCCList...) - - rm := map[string]bool{} - for _, r := range recipients { - rm[r.Address] = true - } - - for _, r := range to { - if !rm[r] { - // Recipient is not known, add it to Bcc. - m.BCCList = append(m.BCCList, &mail.Address{Address: r}) - } - } - - return nil -} - -// Logout is called when this User will no longer be used. -func (su *smtpUser) Logout() error { - log.Debug("SMTP client logged out user ", su.addressID) - return nil -} diff --git a/internal/smtp/utils.go b/internal/smtp/utils.go deleted file mode 100644 index 80847897..00000000 --- a/internal/smtp/utils.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "regexp" -) - -//nolint:gochecknoglobals // Used like a constant -var mailFormat = regexp.MustCompile(`.+@.+\..+`) - -// looksLikeEmail validates whether the string resembles an email. -// -// Notice that it does this naively by simply checking for the existence -// of a DOT and an AT sign. -func looksLikeEmail(e string) bool { - return mailFormat.MatchString(e) -} diff --git a/internal/smtp/vcard_tools.go b/internal/smtp/vcard_tools.go deleted file mode 100644 index b5b6ece0..00000000 --- a/internal/smtp/vcard_tools.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package smtp - -import ( - "encoding/base64" - "strconv" - "strings" - - "github.com/ProtonMail/go-vcard" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -type ContactMetadata struct { - Email string - Keys []string - Scheme string - Sign bool - SignIsSet bool - Encrypt bool - MIMEType string -} - -const ( - FieldPMScheme = "X-PM-SCHEME" - FieldPMEncrypt = "X-PM-ENCRYPT" - FieldPMSign = "X-PM-SIGN" - FieldPMMIMEType = "X-PM-MIMETYPE" -) - -func GetContactMetadataFromVCards(cards []pmapi.Card, email string) (contactMeta *ContactMetadata, err error) { - for _, card := range cards { - dec := vcard.NewDecoder(strings.NewReader(card.Data)) - parsedCard, err := dec.Decode() - if err != nil { - return nil, err - } - group := parsedCard.GetGroupByValue(vcard.FieldEmail, email) - if len(group) == 0 { - continue - } - - keys := []string{} - for _, key := range parsedCard.GetAllValueByGroup(vcard.FieldKey, group) { - keybyte, err := base64.StdEncoding.DecodeString(strings.Split(key, "base64,")[1]) - if err != nil { - return nil, err - } - // It would be better to always have correct data on the server, but mistakes - // can happen -- we had an issue where KEY was included in VCARD, but was empty. - // It's valid and we need to handle it by not including it in the keys, which would fail later. - if len(keybyte) > 0 { - keys = append(keys, string(keybyte)) - } - } - scheme := parsedCard.GetValueByGroup(FieldPMScheme, group) - // Warn: ParseBool treats 1, T, True, true as true and 0, F, Fale, false as false. - // However PMEL declares 'true' is true, 'false' is false. every other string is true - encrypt, _ := strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMEncrypt, group)) - var sign, signIsSet bool - if len(parsedCard[FieldPMSign]) == 0 { - signIsSet = false - } else { - sign, _ = strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMSign, group)) - signIsSet = true - } - mimeType := parsedCard.GetValueByGroup(FieldPMMIMEType, group) - return &ContactMetadata{ - Email: email, - Keys: keys, - Scheme: scheme, - Sign: sign, - SignIsSet: signIsSet, - Encrypt: encrypt, - MIMEType: mimeType, - }, nil - } - return &ContactMetadata{}, nil -} diff --git a/internal/store/address.go b/internal/store/address.go deleted file mode 100644 index b6d083af..00000000 --- a/internal/store/address.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/sirupsen/logrus" - bolt "go.etcd.io/bbolt" -) - -// Address holds mailboxes for IMAP user (login address). In combined mode -// there is only one address, in split mode there is one object per address. -type Address struct { - store *Store - address string - addressID string - mailboxes map[string]*Mailbox - - log *logrus.Entry -} - -func newAddress( - store *Store, - address, addressID string, - labels []*pmapi.Label, -) (addr *Address, err error) { - l := log.WithField("addressID", addressID) - - storeAddress := &Address{ - store: store, - address: address, - addressID: addressID, - log: l, - } - - if err = storeAddress.init(labels); err != nil { - l.WithField("address", address). - WithError(err). - Error("Could not initialise store address") - - return - } - - return storeAddress, nil -} - -func (storeAddress *Address) init(foldersAndLabels []*pmapi.Label) (err error) { - storeAddress.log.WithField("address", storeAddress.address).Debug("Initialising store address") - - storeAddress.mailboxes = make(map[string]*Mailbox) - - err = storeAddress.store.db.Update(func(tx *bolt.Tx) error { - for _, label := range foldersAndLabels { - prefix := getLabelPrefix(label) - - var mailbox *Mailbox - if mailbox, err = txNewMailbox(tx, storeAddress, label.ID, prefix, label.Path, label.Color); err != nil { - storeAddress.log. - WithError(err). - WithField("labelID", label.ID). - Error("Could not init mailbox for folder or label") - return err - } - - storeAddress.mailboxes[label.ID] = mailbox - } - return nil - }) - - return -} - -// getLabelPrefix returns the correct prefix for a pmapi label according to whether it is exclusive or not. -func getLabelPrefix(l *pmapi.Label) string { - switch { - case pmapi.IsSystemLabel(l.ID): - return "" - case bool(l.Exclusive): - return UserFoldersPrefix - default: - return UserLabelsPrefix - } -} - -// AddressString returns the address. -func (storeAddress *Address) AddressString() string { - return storeAddress.address -} - -// AddressID returns the address ID. -func (storeAddress *Address) AddressID() string { - return storeAddress.addressID -} - -// APIAddress returns the `pmapi.Address` struct. -func (storeAddress *Address) APIAddress() *pmapi.Address { - return storeAddress.client().Addresses().ByEmail(storeAddress.address) -} - -func (storeAddress *Address) client() pmapi.Client { - return storeAddress.store.client() -} diff --git a/internal/store/address_mailbox.go b/internal/store/address_mailbox.go deleted file mode 100644 index 06692ee9..00000000 --- a/internal/store/address_mailbox.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "fmt" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -// ListMailboxes returns all mailboxes. -func (storeAddress *Address) ListMailboxes() []*Mailbox { - storeAddress.store.lock.RLock() - defer storeAddress.store.lock.RUnlock() - - mailboxes := make([]*Mailbox, 0, len(storeAddress.mailboxes)) - for _, m := range storeAddress.mailboxes { - mailboxes = append(mailboxes, m) - } - return mailboxes -} - -// GetMailbox returns mailbox with the given IMAP name. -func (storeAddress *Address) GetMailbox(name string) (*Mailbox, error) { - storeAddress.store.lock.RLock() - defer storeAddress.store.lock.RUnlock() - - for _, m := range storeAddress.mailboxes { - if m.Name() == name { - return m, nil - } - } - - return nil, fmt.Errorf("mailbox %v does not exist", name) -} - -// CreateMailbox creates the mailbox by calling an API. -// Mailbox is created in the structure by processing event. -func (storeAddress *Address) CreateMailbox(name string) error { - return storeAddress.store.createMailbox(name) -} - -// updateMailbox updates the mailbox by calling an API. -// Mailbox is updated in the structure by processing event. -func (storeAddress *Address) updateMailbox(labelID, newName, color string) error { - return storeAddress.store.updateMailbox(labelID, newName, color) -} - -// deleteMailbox deletes the mailbox by calling an API. -// Mailbox is deleted in the structure by processing event. -func (storeAddress *Address) deleteMailbox(labelID string) error { - return storeAddress.store.deleteMailbox(labelID, storeAddress.addressID) -} - -// createOrUpdateMailboxEvent creates or updates the mailbox in the structure. -// This is called from the event loop. -func (storeAddress *Address) createOrUpdateMailboxEvent(label *pmapi.Label) error { - prefix := getLabelPrefix(label) - mailbox, ok := storeAddress.mailboxes[label.ID] - if !ok { - mailbox, err := newMailbox(storeAddress, label.ID, prefix, label.Path, label.Color) - if err != nil { - return err - } - storeAddress.mailboxes[label.ID] = mailbox - mailbox.store.notifyMailboxCreated(storeAddress.address, mailbox.labelName) - } else { - mailbox.labelName = prefix + label.Path - mailbox.color = label.Color - } - return nil -} - -// deleteMailboxEvent deletes the mailbox in the structure. -// This is called from the event loop. -func (storeAddress *Address) deleteMailboxEvent(labelID string) error { - storeMailbox, ok := storeAddress.mailboxes[labelID] - if !ok { - log.WithField("labelID", labelID).Warn("Could not find mailbox to delete") - return nil - } - delete(storeAddress.mailboxes, labelID) - return storeMailbox.deleteMailboxEvent() -} - -func (storeAddress *Address) getMailboxByID(labelID string) (*Mailbox, error) { - storeMailbox, ok := storeAddress.mailboxes[labelID] - if !ok { - return nil, fmt.Errorf("mailbox with id %q does not exist", labelID) - } - return storeMailbox, nil -} diff --git a/internal/store/address_message.go b/internal/store/address_message.go deleted file mode 100644 index fc69e488..00000000 --- a/internal/store/address_message.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - bolt "go.etcd.io/bbolt" -) - -func (storeAddress *Address) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi.Message) error { - for _, m := range storeAddress.mailboxes { - if err := m.txCreateOrUpdateMessages(tx, msgs); err != nil { - return err - } - } - return nil -} - -// txDeleteMessage deletes the message from the mailbox buckets for this address. -func (storeAddress *Address) txDeleteMessage(tx *bolt.Tx, apiID string) error { - for _, m := range storeAddress.mailboxes { - if err := m.txDeleteMessage(tx, apiID); err != nil { - return err - } - } - return nil -} diff --git a/internal/store/cache.go b/internal/store/cache.go deleted file mode 100644 index a2b907ca..00000000 --- a/internal/store/cache.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - bolt "go.etcd.io/bbolt" -) - -const passphraseKey = "passphrase" - -// UnlockCache unlocks the cache for the user with the given keyring. -func (store *Store) UnlockCache(kr *crypto.KeyRing) error { - passphrase, err := store.getCachePassphrase() - if err != nil { - return err - } - - if passphrase == nil { - if passphrase, err = crypto.RandomToken(32); err != nil { - return err - } - - enc, err := kr.Encrypt(crypto.NewPlainMessage(passphrase), nil) - if err != nil { - return err - } - - if err := store.setCachePassphrase(enc.GetBinary()); err != nil { - return err - } - } else { - dec, err := kr.Decrypt(crypto.NewPGPMessage(passphrase), nil, crypto.GetUnixTime()) - if err != nil { - return err - } - - passphrase = dec.GetBinary() - } - - if err := store.cache.Unlock(store.user.ID(), passphrase); err != nil { - return err - } - - store.msgCachePool.start() - - return nil -} - -func (store *Store) getCachePassphrase() ([]byte, error) { - var passphrase []byte - - if err := store.db.View(func(tx *bolt.Tx) error { - passphrase = tx.Bucket(cachePassphraseBucket).Get([]byte(passphraseKey)) - return nil - }); err != nil { - return nil, err - } - - return passphrase, nil -} - -func (store *Store) setCachePassphrase(passphrase []byte) error { - return store.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(cachePassphraseBucket).Put([]byte(passphraseKey), passphrase) - }) -} - -func (store *Store) clearCachePassphrase() error { - return store.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(cachePassphraseBucket).Delete([]byte(passphraseKey)) - }) -} - -// buildAndCacheJobs is used to limit the number of parallel background build -// jobs by using a buffered channel. When channel is blocking the go routines -// is running but the download didn't started yet and hence no space needs to -// be allocated. Once other instances are finished the job can continue. The -// bottleneck is `store.cache.Set` which can be take some time to write all -// downloaded bytes. Therefore, it is not effective to start fetching and -// building the message for more than maximum of possible parallel cache -// writers. -// -// Default buildAndCacheJobs vaule is 16, it can be changed by SetBuildAndCacheJobLimit. -var ( - buildAndCacheJobs = make(chan struct{}, 16) //nolint:gochecknoglobals -) - -func SetBuildAndCacheJobLimit(maxJobs int) { - buildAndCacheJobs = make(chan struct{}, maxJobs) -} - -func (store *Store) getCachedMessage(messageID string) ([]byte, error) { - if store.IsCached(messageID) { - literal, err := store.cache.Get(store.user.ID(), messageID) - if err == nil { - return literal, nil - } - store.log. - WithField("msg", messageID). - WithError(err). - Warn("Message is cached but cannot be retrieved") - } - - job, done := store.newBuildJob(context.Background(), messageID, message.ForegroundPriority) - defer done() - - literal, err := job.GetResult() - if err != nil { - store.checkAndRemoveDeletedMessage(err, messageID) - return nil, err - } - - if !store.isMessageADraft(messageID) { - if err := store.writeToCacheUnlockIfFails(messageID, literal); err != nil { - store.log.WithError(err).Error("Failed to cache message") - } - } else { - store.log.Debug("Skipping cache draft message") - } - - return literal, nil -} - -func (store *Store) writeToCacheUnlockIfFails(messageID string, literal []byte) error { - err := store.cache.Set(store.user.ID(), messageID, literal) - if err == nil && err != cache.ErrCacheNeedsUnlock { - return err - } - - kr, err := store.client().GetUserKeyRing() - if err != nil { - return err - } - - if err := store.UnlockCache(kr); err != nil { - return err - } - - return store.cache.Set(store.user.ID(), messageID, literal) -} - -// IsCached returns whether the given message already exists in the cache. -func (store *Store) IsCached(messageID string) (has bool) { - defer func() { - if r := recover(); r != nil { - store.log.WithField("recovered", r).Error("Cannot retrieve whether message exits, assuming no") - } - }() - has = store.cache.Has(store.user.ID(), messageID) - return -} - -// BuildAndCacheMessage builds the given message (with background priority) and puts it in the cache. -// It builds with background priority. -func (store *Store) BuildAndCacheMessage(ctx context.Context, messageID string) error { - buildAndCacheJobs <- struct{}{} - defer func() { <-buildAndCacheJobs }() - - if store.isMessageADraft(messageID) { - return nil - } - - job, done := store.newBuildJob(ctx, messageID, message.BackgroundPriority) - defer done() - - literal, err := job.GetResult() - if err != nil { - store.checkAndRemoveDeletedMessage(err, messageID) - return err - } - - return store.cache.Set(store.user.ID(), messageID, literal) -} - -func (store *Store) checkAndRemoveDeletedMessage(err error, msgID string) { - if !pmapi.IsUnprocessableEntity(err) { - return - } - l := store.log.WithError(err).WithField("msgID", msgID) - l.Warn("Deleting message which was not found on API") - - if deleteErr := store.deleteMessageEvent(msgID); deleteErr != nil { - l.WithField("deleteErr", deleteErr).Error("Failed to delete non-existed API message from DB") - } -} diff --git a/internal/store/cache/cache_test.go b/internal/store/cache/cache_test.go deleted file mode 100644 index 18054d9a..00000000 --- a/internal/store/cache/cache_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -import ( - "runtime" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestOnDiskCacheNoCompression(t *testing.T) { - cache, err := NewOnDiskCache(t.TempDir(), &NoopCompressor{}, Options{ConcurrentRead: runtime.NumCPU(), ConcurrentWrite: runtime.NumCPU()}) - require.NoError(t, err) - - testCache(t, cache) -} - -func TestOnDiskCacheGZipCompression(t *testing.T) { - cache, err := NewOnDiskCache(t.TempDir(), &GZipCompressor{}, Options{ConcurrentRead: runtime.NumCPU(), ConcurrentWrite: runtime.NumCPU()}) - require.NoError(t, err) - - testCache(t, cache) -} - -func TestInMemoryCache(t *testing.T) { - testCache(t, NewInMemoryCache(1<<20)) -} - -func testCache(t *testing.T, cache Cache) { - assert.NoError(t, cache.Unlock("userID1", []byte("my secret passphrase"))) - assert.NoError(t, cache.Unlock("userID2", []byte("my other passphrase"))) - - getSetCachedMessage(t, cache, "userID1", "messageID1", "some secret") - assert.True(t, cache.Has("userID1", "messageID1")) - - getSetCachedMessage(t, cache, "userID2", "messageID2", "some other secret") - assert.True(t, cache.Has("userID2", "messageID2")) - - assert.NoError(t, cache.Rem("userID1", "messageID1")) - assert.False(t, cache.Has("userID1", "messageID1")) - - assert.NoError(t, cache.Rem("userID2", "messageID2")) - assert.False(t, cache.Has("userID2", "messageID2")) - - assert.NoError(t, cache.Delete("userID1")) - assert.NoError(t, cache.Delete("userID2")) -} - -func getSetCachedMessage(t *testing.T, cache Cache, userID, messageID, secret string) { - assert.NoError(t, cache.Set(userID, messageID, []byte(secret))) - - data, err := cache.Get(userID, messageID) - assert.NoError(t, err) - - assert.Equal(t, []byte(secret), data) -} diff --git a/internal/store/cache/compressor.go b/internal/store/cache/compressor.go deleted file mode 100644 index 9a52902c..00000000 --- a/internal/store/cache/compressor.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -type Compressor interface { - Compress([]byte) ([]byte, error) - Decompress([]byte) ([]byte, error) -} - -type NoopCompressor struct{} - -func (NoopCompressor) Compress(dec []byte) ([]byte, error) { - return dec, nil -} - -func (NoopCompressor) Decompress(cmp []byte) ([]byte, error) { - return cmp, nil -} diff --git a/internal/store/cache/compressor_gzip.go b/internal/store/cache/compressor_gzip.go deleted file mode 100644 index 68acde08..00000000 --- a/internal/store/cache/compressor_gzip.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -import ( - "bytes" - "compress/gzip" -) - -type GZipCompressor struct{} - -func (GZipCompressor) Compress(dec []byte) ([]byte, error) { - buf := new(bytes.Buffer) - - zw := gzip.NewWriter(buf) - - if _, err := zw.Write(dec); err != nil { - return nil, err - } - - if err := zw.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func (GZipCompressor) Decompress(cmp []byte) ([]byte, error) { - zr, err := gzip.NewReader(bytes.NewReader(cmp)) - if err != nil { - return nil, err - } - - buf := new(bytes.Buffer) - - if _, err := buf.ReadFrom(zr); err != nil { - return nil, err - } - - if err := zr.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} diff --git a/internal/store/cache/disk.go b/internal/store/cache/disk.go deleted file mode 100644 index 82cfcb50..00000000 --- a/internal/store/cache/disk.go +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - - "github.com/ProtonMail/proton-bridge/v2/pkg/algo" - "github.com/ProtonMail/proton-bridge/v2/pkg/semaphore" - "github.com/ricochet2200/go-disk-usage/du" -) - -var ( - ErrMsgCorrupted = errors.New("ecrypted file was corrupted") - ErrLowSpace = errors.New("not enough free space left on device") -) - -// IsOnDiskCache will return true if Cache is type of onDiskCache. -func IsOnDiskCache(c Cache) bool { - _, ok := c.(*onDiskCache) - return ok -} - -type onDiskCache struct { - path string - opts Options - - gcm map[string]cipher.AEAD - cmp Compressor - rsem, wsem semaphore.Semaphore - pending *pending - - diskSize uint64 - diskFree uint64 - once *sync.Once - lock sync.Mutex -} - -func NewOnDiskCache(path string, cmp Compressor, opts Options) (Cache, error) { - if err := os.MkdirAll(path, 0o700); err != nil { - return nil, err - } - - file, err := os.CreateTemp(path, "tmp") - defer func() { - file.Close() //nolint:errcheck,gosec - os.Remove(file.Name()) //nolint:errcheck,gosec - }() - if err != nil { - return nil, fmt.Errorf("cannot open test write target: %w", err) - } - if _, err := file.Write([]byte("test-write")); err != nil { - return nil, fmt.Errorf("cannot write to target: %w", err) - } - - usage := du.NewDiskUsage(path) - - // NOTE(GODT-1158): use Available() or Free()? - return &onDiskCache{ - path: path, - opts: opts, - - gcm: make(map[string]cipher.AEAD), - cmp: cmp, - rsem: semaphore.New(opts.ConcurrentRead), - wsem: semaphore.New(opts.ConcurrentWrite), - pending: newPending(), - - diskSize: usage.Size(), - diskFree: usage.Available(), - once: &sync.Once{}, - }, nil -} - -func (c *onDiskCache) Lock(userID string) { - delete(c.gcm, userID) -} - -func (c *onDiskCache) Unlock(userID string, passphrase []byte) error { - aes, err := aes.NewCipher(algo.Hash256(passphrase)) - if err != nil { - return err - } - - gcm, err := cipher.NewGCM(aes) - if err != nil { - return err - } - - if err := os.MkdirAll(c.getUserPath(userID), 0o700); err != nil { - return err - } - - c.gcm[userID] = gcm - - return nil -} - -func (c *onDiskCache) Delete(userID string) error { - defer c.update() - - return os.RemoveAll(c.getUserPath(userID)) -} - -// Has returns whether the given message exists in the cache. -func (c *onDiskCache) Has(userID, messageID string) bool { - c.pending.wait(c.getMessagePath(userID, messageID)) - - c.rsem.Lock() - defer c.rsem.Unlock() - - _, err := os.Stat(c.getMessagePath(userID, messageID)) - - switch { - case err == nil: - return true - - case os.IsNotExist(err): - return false - - default: - // Cannot decide whether the message is cached or not. - // Potential recover needs to be don in caller function. - panic(err) - } -} - -func (c *onDiskCache) Get(userID, messageID string) ([]byte, error) { - gcm, ok := c.gcm[userID] - if !ok || gcm == nil { - return nil, ErrCacheNeedsUnlock - } - - enc, err := c.readFile(c.getMessagePath(userID, messageID)) - if err != nil { - return nil, err - } - - // Data stored in file must larger than NonceSize. - if len(enc) <= gcm.NonceSize() { - return nil, ErrMsgCorrupted - } - - cmp, err := gcm.Open(nil, enc[:gcm.NonceSize()], enc[gcm.NonceSize():], nil) - if err != nil { - return nil, err - } - - return c.cmp.Decompress(cmp) -} - -func (c *onDiskCache) Set(userID, messageID string, literal []byte) error { - gcm, ok := c.gcm[userID] - if !ok { - return ErrCacheNeedsUnlock - } - nonce := make([]byte, gcm.NonceSize()) - - if _, err := rand.Read(nonce); err != nil { - return err - } - - cmp, err := c.cmp.Compress(literal) - if err != nil { - return err - } - - // NOTE(GODT-1158, GODT-1488): Need to properly handle low space. Don't - // return error, that's bad. Send event and clean least used message. - if !c.hasSpace(len(cmp)) { - return nil - } - - return c.writeFile(c.getMessagePath(userID, messageID), gcm.Seal(nonce, nonce, cmp, nil)) -} - -func (c *onDiskCache) Rem(userID, messageID string) error { - defer c.update() - - return os.Remove(c.getMessagePath(userID, messageID)) -} - -func (c *onDiskCache) readFile(path string) ([]byte, error) { - c.rsem.Lock() - defer c.rsem.Unlock() - - // Wait before reading in case the file is currently being written. - c.pending.wait(path) - - return os.ReadFile(filepath.Clean(path)) -} - -func (c *onDiskCache) writeFile(path string, b []byte) error { - c.wsem.Lock() - defer c.wsem.Unlock() - - // Mark the file as currently being written. - // If it's already being written, wait for it to be done and return nil. - // NOTE(GODT-1158): Let's hope it succeeded... - if ok := c.pending.add(path); !ok { - c.pending.wait(path) - return nil - } - defer c.pending.done(path) - - // Reduce the approximate free space (update it exactly later). - c.lock.Lock() - c.diskFree -= uint64(len(b)) - c.lock.Unlock() - - // Update the diskFree eventually. - defer c.update() - - // NOTE(GODT-1158): What happens when this fails? Should be fixed eventually. - return os.WriteFile(filepath.Clean(path), b, 0o600) -} - -func (c *onDiskCache) hasSpace(size int) bool { - c.lock.Lock() - defer c.lock.Unlock() - - if c.opts.MinFreeAbs > 0 { - if c.diskFree-uint64(size) < c.opts.MinFreeAbs { - return false - } - } - - if c.opts.MinFreeRat > 0 { - if float64(c.diskFree-uint64(size))/float64(c.diskSize) < c.opts.MinFreeRat { - return false - } - } - - return true -} - -func (c *onDiskCache) update() { - go func() { - c.once.Do(func() { - c.lock.Lock() - defer c.lock.Unlock() - - // Update the free space. - c.diskFree = du.NewDiskUsage(c.path).Available() - - // Reset the Once object (so we can update again). - c.once = &sync.Once{} - }) - }() -} - -func (c *onDiskCache) getUserPath(userID string) string { - return filepath.Join(c.path, algo.HashHexSHA256(userID)) -} - -func (c *onDiskCache) getMessagePath(userID, messageID string) string { - return filepath.Join(c.getUserPath(userID), algo.HashHexSHA256(messageID)) -} diff --git a/internal/store/cache/memory.go b/internal/store/cache/memory.go deleted file mode 100644 index f2e2ee52..00000000 --- a/internal/store/cache/memory.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -import ( - "errors" - "sync" -) - -type inMemoryCache struct { - lock sync.RWMutex - data map[string]map[string][]byte - size, limit int -} - -// NewInMemoryCache creates a new in memory cache which stores up to the given -// number of bytes of cached data. -// NOTE(GODT-1158): Make this threadsafe. -func NewInMemoryCache(limit int) Cache { - return &inMemoryCache{ - data: make(map[string]map[string][]byte), - limit: limit, - } -} - -func (c *inMemoryCache) Unlock(userID string, passphrase []byte) error { - c.data[userID] = make(map[string][]byte) - return nil -} - -func (c *inMemoryCache) Lock(userID string) { - c.lock.Lock() - defer c.lock.Unlock() - - for _, message := range c.data[userID] { - c.size -= len(message) - } - - delete(c.data, userID) -} - -func (c *inMemoryCache) Delete(userID string) error { - c.Lock(userID) - return nil -} - -// Has returns whether the given message exists in the cache. -func (c *inMemoryCache) Has(userID, messageID string) bool { - c.lock.RLock() - defer c.lock.RUnlock() - - if !c.isUserUnlocked(userID) { - // This might look counter intuitive but in order to be able to test - // "re-unlocking" mechanism we need to return true here. - // - // The situation is the same as it would happen for onDiskCache with - // locked user. Later during `Get` cache would return proper error - // `ErrCacheNeedsUnlock`. It is expected that store would then try to - // re-unlock. - // - // In order to do proper behaviour we should implement - // encryption for inMemoryCache. - return true - } - - _, ok := c.data[userID][messageID] - return ok -} - -func (c *inMemoryCache) Get(userID, messageID string) ([]byte, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - if !c.isUserUnlocked(userID) { - return nil, ErrCacheNeedsUnlock - } - - literal, ok := c.data[userID][messageID] - if !ok { - return nil, errors.New("no such message in cache") - } - - return literal, nil -} - -func (c *inMemoryCache) isUserUnlocked(userID string) bool { - _, ok := c.data[userID] - return ok -} - -// Set saves the message literal to memory for further usage. -// -// NOTE(GODT-1158, GODT-1488): Once memory limit is reached we should do proper -// rotation based on usage frequency. -func (c *inMemoryCache) Set(userID, messageID string, literal []byte) error { - c.lock.Lock() - defer c.lock.Unlock() - - if !c.isUserUnlocked(userID) { - return ErrCacheNeedsUnlock - } - - if c.size+len(literal) > c.limit { - return nil - } - - c.size += len(literal) - c.data[userID][messageID] = literal - - return nil -} - -func (c *inMemoryCache) Rem(userID, messageID string) error { - c.lock.Lock() - defer c.lock.Unlock() - - if !c.isUserUnlocked(userID) { - return nil - } - - c.size -= len(c.data[userID][messageID]) - - delete(c.data[userID], messageID) - - return nil -} diff --git a/internal/store/cache/options.go b/internal/store/cache/options.go deleted file mode 100644 index 26b25664..00000000 --- a/internal/store/cache/options.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -type Options struct { - MinFreeAbs uint64 - MinFreeRat float64 - ConcurrentRead int - ConcurrentWrite int -} diff --git a/internal/store/cache/pending.go b/internal/store/cache/pending.go deleted file mode 100644 index 6fd0dd69..00000000 --- a/internal/store/cache/pending.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -import "sync" - -type pending struct { - lock sync.Mutex - path map[string]chan struct{} -} - -func newPending() *pending { - return &pending{path: make(map[string]chan struct{})} -} - -func (p *pending) add(path string) bool { - p.lock.Lock() - defer p.lock.Unlock() - - if _, ok := p.path[path]; ok { - return false - } - - p.path[path] = make(chan struct{}) - - return true -} - -func (p *pending) wait(path string) { - p.lock.Lock() - ch, ok := p.path[path] - p.lock.Unlock() - - if ok { - <-ch - } -} - -func (p *pending) done(path string) { - p.lock.Lock() - defer p.lock.Unlock() - - defer close(p.path[path]) - - delete(p.path, path) -} diff --git a/internal/store/cache/pending_test.go b/internal/store/cache/pending_test.go deleted file mode 100644 index b58e14e7..00000000 --- a/internal/store/cache/pending_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPending(t *testing.T) { - pending := newPending() - - pending.add("1") - pending.add("2") - pending.add("3") - - resCh := make(chan string) - - go func() { pending.wait("1"); resCh <- "1" }() - go func() { pending.wait("2"); resCh <- "2" }() - go func() { pending.wait("3"); resCh <- "3" }() - - pending.done("1") - assert.Equal(t, "1", <-resCh) - - pending.done("2") - assert.Equal(t, "2", <-resCh) - - pending.done("3") - assert.Equal(t, "3", <-resCh) -} - -func TestPendingUnknown(t *testing.T) { - newPending().wait("this is not currently being waited") -} diff --git a/internal/store/cache/types.go b/internal/store/cache/types.go deleted file mode 100644 index dd83a191..00000000 --- a/internal/store/cache/types.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cache - -import "errors" - -var ErrCacheNeedsUnlock = errors.New("cache needs to be unlocked") - -type Cache interface { - Unlock(userID string, passphrase []byte) error - Lock(userID string) - Delete(userID string) error - - Has(userID, messageID string) bool - Get(userID, messageID string) ([]byte, error) - Set(userID, messageID string, literal []byte) error - Rem(userID, messageID string) error -} diff --git a/internal/store/cache_test.go b/internal/store/cache_test.go deleted file mode 100644 index 6ff67ce2..00000000 --- a/internal/store/cache_test.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "testing" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -func TestIsCachedCrashRecovers(t *testing.T) { - r := require.New(t) - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true, &pmapi.Message{ - ID: "msg1", - Subject: "subject", - }) - - r.False(m.store.IsCached("msg1")) - - m.store.cache = nil - r.False(m.store.IsCached("msg1")) -} - -var wantLiteral = []byte("Mime-Version: 1.0\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: \r\nReferences: \r\nX-Pm-Date: Thu, 01 Jan 1970 00:00:00 +0000\r\nX-Pm-External-Id: <>\r\nX-Pm-Internal-Id: msg1\r\nX-Original-Date: Mon, 01 Jan 0001 00:00:00 +0000\r\nDate: Fri, 13 Aug 1982 00:00:00 +0000\r\nMessage-Id: \r\nSubject: subject\r\n\r\n") - -func TestGetCachedMessageOK(t *testing.T) { - r := require.New(t) - m, clear := initMocks(t) - defer clear() - - messageID := "msg1" - - m.newStoreNoEvents(t, true, &pmapi.Message{ - ID: messageID, - Subject: "subject", - Flags: pmapi.FlagReceived, - Body: "body", - }) - - // Have build job - m.client.EXPECT(). - KeyRingForAddressID(gomock.Any()). - Return(testPrivateKeyRing, nil). - Times(1) - - haveLiteral, err := m.store.getCachedMessage(messageID) - r.NoError(err) - r.Equal(wantLiteral, haveLiteral) - - r.True(m.store.IsCached(messageID)) - - // No build job - haveLiteral, err = m.store.getCachedMessage(messageID) - r.NoError(err) - r.Equal(wantLiteral, haveLiteral) - r.True(m.store.IsCached(messageID)) -} - -func TestGetCachedMessageCacheLocked(t *testing.T) { - r := require.New(t) - m, clear := initMocks(t) - defer clear() - - messageID := "msg1" - - m.newStoreNoEvents(t, true, &pmapi.Message{ - ID: messageID, - Subject: "subject", - Flags: pmapi.FlagReceived, - Body: "body", - }) - - // Have build job - m.client.EXPECT(). - KeyRingForAddressID(gomock.Any()). - Return(testPrivateKeyRing, nil). - Times(1) - haveLiteral, err := m.store.getCachedMessage(messageID) - r.NoError(err) - r.Equal(wantLiteral, haveLiteral) - r.True(m.store.IsCached(messageID)) - - // Lock cache - m.store.cache.Lock(m.store.user.ID()) - - // Have build job again due to failure - m.client.EXPECT(). - KeyRingForAddressID(gomock.Any()). - Return(testPrivateKeyRing, nil). - Times(1) - - haveLiteral, err = m.store.getCachedMessage(messageID) - r.NoError(err) - r.Equal(wantLiteral, haveLiteral) - r.True(m.store.IsCached(messageID)) - - // No build job - haveLiteral, err = m.store.getCachedMessage(messageID) - r.NoError(err) - r.Equal(wantLiteral, haveLiteral) - r.True(m.store.IsCached(messageID)) -} diff --git a/internal/store/cache_watcher.go b/internal/store/cache_watcher.go deleted file mode 100644 index fd119772..00000000 --- a/internal/store/cache_watcher.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" -) - -func (store *Store) StartWatcher() { - if !cache.IsOnDiskCache(store.cache) { - return - } - - store.done = make(chan struct{}) - - ctx, cancel := context.WithCancel(context.Background()) - store.msgCachePool.ctx = ctx - - go func() { - ticker := time.NewTicker(10 * time.Minute) - defer ticker.Stop() - defer cancel() - - for { - // NOTE(GODT-1158): Race condition here? What if DB was already closed? - messageIDs, err := store.getAllMessageIDs() - if err != nil { - return - } - - for _, messageID := range messageIDs { - if !store.IsCached(messageID) { - store.msgCachePool.newJob(messageID) - } - } - - select { - case <-store.done: - return - case <-ticker.C: - continue - } - } - }() -} - -func (store *Store) stopWatcher() { - if store.done == nil { - return - } - - select { - default: - close(store.done) - - case <-store.done: - return - } -} diff --git a/internal/store/cache_worker.go b/internal/store/cache_worker.go deleted file mode 100644 index 01b5b0c7..00000000 --- a/internal/store/cache_worker.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - "sync" - - "github.com/sirupsen/logrus" -) - -type MsgCachePool struct { - storer Storer - jobs chan string - done chan struct{} - started bool - wg *sync.WaitGroup - ctx context.Context -} - -type Storer interface { - IsCached(messageID string) bool - BuildAndCacheMessage(ctx context.Context, messageID string) error -} - -func newMsgCachePool(storer Storer) *MsgCachePool { - return &MsgCachePool{ - storer: storer, - jobs: make(chan string), - done: make(chan struct{}), - wg: &sync.WaitGroup{}, - ctx: context.Background(), - } -} - -// newJob sends a new job to the cacher if it's running. -func (cacher *MsgCachePool) newJob(messageID string) { - if !cacher.started { - return - } - - select { - case <-cacher.done: - return - - default: - if !cacher.storer.IsCached(messageID) { - cacher.wg.Add(1) - go func() { cacher.jobs <- messageID }() - } - } -} - -func (cacher *MsgCachePool) start() { - if cacher.started { - return - } - - cacher.started = true - - go func() { - for { - select { - case messageID := <-cacher.jobs: - go cacher.handleJob(messageID) - - case <-cacher.done: - return - } - } - }() -} - -func (cacher *MsgCachePool) handleJob(messageID string) { - defer cacher.wg.Done() - - if err := cacher.storer.BuildAndCacheMessage(cacher.ctx, messageID); err != nil { - logrus.WithError(err).Error("Failed to build and cache message") - } else { - logrus.WithField("messageID", messageID).Trace("Message cached") - } -} - -func (cacher *MsgCachePool) stop() { - cacher.started = false - - cacher.wg.Wait() - - select { - case <-cacher.done: - return - - default: - close(cacher.done) - } -} diff --git a/internal/store/cache_worker_test.go b/internal/store/cache_worker_test.go deleted file mode 100644 index 31a21863..00000000 --- a/internal/store/cache_worker_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "testing" - - storemocks "github.com/ProtonMail/proton-bridge/v2/internal/store/mocks" - "github.com/golang/mock/gomock" - "github.com/pkg/errors" -) - -func withTestCacher(t *testing.T, doTest func(storer *storemocks.MockStorer, cacher *MsgCachePool)) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Mock storer used to build/cache messages. - storer := storemocks.NewMockStorer(ctrl) - - // Create a new cacher pointing to the fake store. - cacher := newMsgCachePool(storer) - - // Start the cacher and wait for it to stop. - cacher.start() - defer cacher.stop() - - doTest(storer, cacher) -} - -func TestCacher(t *testing.T) { - // If the message is not yet cached, we should expect to try to build and cache it. - withTestCacher(t, func(storer *storemocks.MockStorer, cacher *MsgCachePool) { - storer.EXPECT().IsCached("messageID").Return(false) - storer.EXPECT().BuildAndCacheMessage(cacher.ctx, "messageID").Return(nil) - cacher.newJob("messageID") - }) -} - -func TestCacherAlreadyCached(t *testing.T) { - // If the message is already cached, we should not try to build it. - withTestCacher(t, func(storer *storemocks.MockStorer, cacher *MsgCachePool) { - storer.EXPECT().IsCached("messageID").Return(true) - cacher.newJob("messageID") - }) -} - -func TestCacherFail(t *testing.T) { - // If building the message fails, we should not try to cache it. - withTestCacher(t, func(storer *storemocks.MockStorer, cacher *MsgCachePool) { - storer.EXPECT().IsCached("messageID").Return(false) - storer.EXPECT().BuildAndCacheMessage(cacher.ctx, "messageID").Return(errors.New("failed to build message")) - cacher.newJob("messageID") - }) -} - -func TestCacherStop(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Mock storer used to build/cache messages. - storer := storemocks.NewMockStorer(ctrl) - - // Create a new cacher pointing to the fake store. - cacher := newMsgCachePool(storer) - - // Start the cacher. - cacher.start() - - // Send a job -- this should succeed. - storer.EXPECT().IsCached("messageID").Return(false) - storer.EXPECT().BuildAndCacheMessage(cacher.ctx, "messageID").Return(nil) - cacher.newJob("messageID") - - // Stop the cacher. - cacher.stop() - - // Send more jobs -- these should all be dropped. - cacher.newJob("messageID2") - cacher.newJob("messageID3") - cacher.newJob("messageID4") - cacher.newJob("messageID5") - - // Stopping the cacher multiple times is safe. - cacher.stop() - cacher.stop() - cacher.stop() - cacher.stop() -} diff --git a/internal/store/change.go b/internal/store/change.go deleted file mode 100644 index 9045c254..00000000 --- a/internal/store/change.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -type ChangeNotifier interface { - Notice(address, notice string) - UpdateMessage( - address, mailboxName string, - uid, sequenceNumber uint32, - msg *pmapi.Message, hasDeletedFlag bool) - DeleteMessage(address, mailboxName string, sequenceNumber uint32) - MailboxCreated(address, mailboxName string) - MailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint32) - - CanDelete(mailboxID string) (bool, func()) -} - -// SetChangeNotifier sets notifier to be called once mailbox or message changes. -func (store *Store) SetChangeNotifier(notifier ChangeNotifier) { - store.notifier = notifier -} - -func (store *Store) notifyNotice(address, notice string) { - if store.notifier == nil { - return - } - store.notifier.Notice(address, notice) -} - -func (store *Store) notifyUpdateMessage(address, mailboxName string, uid, sequenceNumber uint32, msg *pmapi.Message, hasDeletedFlag bool) { - if store.notifier == nil { - return - } - store.notifier.UpdateMessage(address, mailboxName, uid, sequenceNumber, msg, hasDeletedFlag) -} - -func (store *Store) notifyDeleteMessage(address, mailboxName string, sequenceNumber uint32) { - if store.notifier == nil { - return - } - store.notifier.DeleteMessage(address, mailboxName, sequenceNumber) -} - -func (store *Store) notifyMailboxCreated(address, mailboxName string) { - if store.notifier == nil { - return - } - store.notifier.MailboxCreated(address, mailboxName) -} - -func (store *Store) notifyMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) { - if store.notifier == nil { - return - } - store.notifier.MailboxStatus(address, mailboxName, uint32(total), uint32(unread), uint32(unreadSeqNum)) -} diff --git a/internal/store/change_test.go b/internal/store/change_test.go deleted file mode 100644 index d2874de2..00000000 --- a/internal/store/change_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "testing" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -func TestNotifyChangeCreateOrUpdateMessage(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.changeNotifier.EXPECT().MailboxStatus(addr1, "All Mail", uint32(1), uint32(0), uint32(0)) - m.changeNotifier.EXPECT().MailboxStatus(addr1, "All Mail", uint32(2), uint32(0), uint32(0)) - m.changeNotifier.EXPECT().UpdateMessage(addr1, "All Mail", uint32(1), uint32(1), gomock.Any(), false) - m.changeNotifier.EXPECT().UpdateMessage(addr1, "All Mail", uint32(2), uint32(2), gomock.Any(), false) - - m.newStoreNoEvents(t, true) - m.store.SetChangeNotifier(m.changeNotifier) - - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel}) - insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel}) -} - -func TestNotifyChangeCreateOrUpdateMessages(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.changeNotifier.EXPECT().MailboxStatus(addr1, "All Mail", uint32(2), uint32(0), uint32(0)) - m.changeNotifier.EXPECT().UpdateMessage(addr1, "All Mail", uint32(1), uint32(1), gomock.Any(), false) - m.changeNotifier.EXPECT().UpdateMessage(addr1, "All Mail", uint32(2), uint32(2), gomock.Any(), false) - - m.newStoreNoEvents(t, true) - m.store.SetChangeNotifier(m.changeNotifier) - - msg1 := getTestMessage("msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel}) - msg2 := getTestMessage("msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel}) - require.Nil(t, m.store.createOrUpdateMessagesEvent([]*pmapi.Message{msg1, msg2})) -} - -func TestNotifyChangeDeleteMessage(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true) - - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel}) - insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel}) - - m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(2)) - m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(1)) - - m.store.SetChangeNotifier(m.changeNotifier) - require.Nil(t, m.store.deleteMessageEvent("msg2")) - require.Nil(t, m.store.deleteMessageEvent("msg1")) -} diff --git a/internal/store/convert.go b/internal/store/convert.go deleted file mode 100644 index abc8933f..00000000 --- a/internal/store/convert.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import "encoding/binary" - -// itob returns a 4-byte big endian representation of v. -func itob(v uint32) []byte { - b := make([]byte, 4) - binary.BigEndian.PutUint32(b, v) - return b -} - -// btoi returns the uint32 represented by b. -func btoi(b []byte) uint32 { - return binary.BigEndian.Uint32(b) -} diff --git a/internal/store/cooldown.go b/internal/store/cooldown.go deleted file mode 100644 index 2f7dd51a..00000000 --- a/internal/store/cooldown.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import "time" - -type cooldown struct { - waitTimes []time.Duration - waitIndex int - lastTry time.Time -} - -func (c *cooldown) setExponentialWait(initial time.Duration, base int, maximum time.Duration) { - waitTimes := []time.Duration{} - t := initial - if base > 1 { - for t < maximum { - waitTimes = append(waitTimes, t) - t *= time.Duration(base) - } - } - waitTimes = append(waitTimes, maximum) - c.setWaitTimes(waitTimes...) -} - -func (c *cooldown) setWaitTimes(newTimes ...time.Duration) { - c.waitTimes = newTimes - c.reset() -} - -// isTooSoon™ returns whether the cooldown period is not yet over. -func (c *cooldown) isTooSoon() bool { - if time.Since(c.lastTry) < c.waitTimes[c.waitIndex] { - return true - } - c.lastTry = time.Now() - return false -} - -func (c *cooldown) increaseWaitTime() { - c.lastTry = time.Now() - if c.waitIndex+1 < len(c.waitTimes) { - c.waitIndex++ - } -} - -func (c *cooldown) reset() { - c.waitIndex = 0 - c.lastTry = time.Time{} -} diff --git a/internal/store/cooldown_test.go b/internal/store/cooldown_test.go deleted file mode 100644 index bbb87a72..00000000 --- a/internal/store/cooldown_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestCooldownExponentialWait(t *testing.T) { - ms := time.Millisecond - sec := time.Second - - testData := []struct { - haveInitial, haveMax time.Duration - haveBase int - wantWaitTimes []time.Duration - }{ - { - haveInitial: 1 * sec, - haveBase: 0, - haveMax: 0 * sec, - wantWaitTimes: []time.Duration{0 * sec}, - }, - { - haveInitial: 0 * sec, - haveBase: 1, - haveMax: 0 * sec, - wantWaitTimes: []time.Duration{0 * sec}, - }, - { - haveInitial: 0 * sec, - haveBase: 0, - haveMax: 1 * sec, - wantWaitTimes: []time.Duration{1 * sec}, - }, - { - haveInitial: 0 * sec, - haveBase: 1, - haveMax: 1 * sec, - wantWaitTimes: []time.Duration{1 * sec}, - }, - { - haveInitial: 1 * sec, - haveBase: 0, - haveMax: 1 * sec, - wantWaitTimes: []time.Duration{1 * sec}, - }, - { - haveInitial: 1 * sec, - haveBase: 2, - haveMax: 1 * sec, - wantWaitTimes: []time.Duration{1 * sec}, - }, - { - haveInitial: 500 * ms, - haveBase: 2, - haveMax: 5 * sec, - wantWaitTimes: []time.Duration{500 * ms, 1 * sec, 2 * sec, 4 * sec, 5 * sec}, - }, - } - - var testCooldown cooldown - - for _, td := range testData { - testCooldown.setExponentialWait(td.haveInitial, td.haveBase, td.haveMax) - assert.Equal(t, td.wantWaitTimes, testCooldown.waitTimes) - } -} - -func TestCooldownIncreaseAndReset(t *testing.T) { - var testCooldown cooldown - testCooldown.setWaitTimes(1*time.Second, 2*time.Second, 3*time.Second) - assert.Equal(t, 0, testCooldown.waitIndex) - - assert.False(t, testCooldown.isTooSoon()) - assert.True(t, testCooldown.isTooSoon()) - assert.Equal(t, 0, testCooldown.waitIndex) - - testCooldown.reset() - assert.Equal(t, 0, testCooldown.waitIndex) - - assert.False(t, testCooldown.isTooSoon()) - assert.True(t, testCooldown.isTooSoon()) - assert.Equal(t, 0, testCooldown.waitIndex) - - // increase at least N+1 times to check overflow - testCooldown.increaseWaitTime() - assert.True(t, testCooldown.isTooSoon()) - testCooldown.increaseWaitTime() - assert.True(t, testCooldown.isTooSoon()) - testCooldown.increaseWaitTime() - assert.True(t, testCooldown.isTooSoon()) - testCooldown.increaseWaitTime() - assert.True(t, testCooldown.isTooSoon()) - - assert.Equal(t, 2, testCooldown.waitIndex) -} - -func TestCooldownNotSooner(t *testing.T) { - var testCooldown cooldown - waitTime := 100 * time.Millisecond - testCooldown.setWaitTimes(waitTime) - - // First time it should never be too soon. - assert.False(t, testCooldown.isTooSoon()) - - // Only half of given wait time should be too soon. - time.Sleep(waitTime / 2) - assert.True(t, testCooldown.isTooSoon()) - - // After given wait time it shouldn't be soon anymore. - time.Sleep(waitTime/2 + time.Millisecond) - assert.False(t, testCooldown.isTooSoon()) -} diff --git a/internal/store/event_loop.go b/internal/store/event_loop.go deleted file mode 100644 index 54c76dd0..00000000 --- a/internal/store/event_loop.go +++ /dev/null @@ -1,638 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - "math/rand" - "time" - - bridgeEvents "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const ( - pollInterval = 30 * time.Second - pollIntervalSpread = 5 * time.Second - - // errMaxSentry defines after how many errors in a row to report it to sentry. - errMaxSentry = 20 -) - -type eventLoop struct { - currentEvents *Events - currentEventID string - currentEvent *pmapi.Event - pollCh chan chan struct{} - stopCh chan struct{} - notifyStopCh chan struct{} - isRunning bool // The whole event loop is running. - - pollCounter int - errCounter int - - log *logrus.Entry - - store *Store - user BridgeUser - listener listener.Listener -} - -func newEventLoop(currentEvents *Events, store *Store, user BridgeUser, listener listener.Listener) *eventLoop { - eventLog := log.WithField("userID", user.ID()) - eventLog.Trace("Creating new event loop") - - return &eventLoop{ - currentEvents: currentEvents, - currentEventID: currentEvents.getEventID(user.ID()), - pollCh: make(chan chan struct{}), - isRunning: false, - - log: eventLog, - - store: store, - user: user, - listener: listener, - } -} - -func (loop *eventLoop) client() pmapi.Client { - return loop.store.client() -} - -func (loop *eventLoop) setFirstEventID() (err error) { - loop.log.Info("Setting first event ID") - - event, err := loop.client().GetEvent(pmapi.ContextWithoutRetry(context.Background()), "") - if err != nil { - loop.log.WithError(err).Error("Could not get latest event ID") - return - } - - loop.currentEventID = event.EventID - - if err = loop.currentEvents.setEventID(loop.user.ID(), loop.currentEventID); err != nil { - loop.log.WithError(err).Error("Could not set latest event ID in user cache") - return - } - - return -} - -// pollNow starts polling events right away and waits till the events are -// processed so we are sure updates are propagated to the database. -func (loop *eventLoop) pollNow() { - // When event loop is not running, it would cause infinite wait. - if !loop.isRunning { - return - } - - eventProcessedCh := make(chan struct{}) - loop.pollCh <- eventProcessedCh - <-eventProcessedCh - close(eventProcessedCh) -} - -func (loop *eventLoop) stop() { - if loop.isRunning { - loop.isRunning = false - close(loop.stopCh) - - select { - case <-loop.notifyStopCh: - loop.log.Warn("Event loop was stopped") - case <-time.After(1 * time.Second): - loop.log.Warn("Timed out waiting for event loop to stop") - } - } -} - -func (loop *eventLoop) start() { - if loop.isRunning { - return - } - defer func() { - loop.isRunning = false - }() - loop.stopCh = make(chan struct{}) - loop.notifyStopCh = make(chan struct{}) - loop.isRunning = true - - events := make(chan *pmapi.Event) - defer close(events) - - loop.log.WithField("lastEventID", loop.currentEventID).Info("Subscribed to events") - defer func() { - loop.log.WithField("lastEventID", loop.currentEventID).Warn("Subscription stopped") - }() - - go loop.pollNow() - - loop.loop() -} - -// loop is the main body of the event loop. -func (loop *eventLoop) loop() { - t := time.NewTicker(pollInterval - pollIntervalSpread) - defer t.Stop() - - for { - var eventProcessedCh chan struct{} - select { - case <-loop.stopCh: - close(loop.notifyStopCh) - return - case <-t.C: - // Randomise periodic calls within range pollInterval ± pollSpread to reduces potential load spikes on API. - //nolint:gosec // It is OK to use weaker random number generator here - time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond) - case eventProcessedCh = <-loop.pollCh: - // We don't want to wait here. Polling should happen instantly. - } - - // Before we fetch the first event, check whether this is the first time we've - // started the event loop, and if so, trigger a full sync. - // In case internet connection was not available during start, it will be - // handled anyway when the connection is back here. - if loop.isBeforeFirstStart() { - if eventErr := loop.setFirstEventID(); eventErr != nil { - loop.log.WithError(eventErr).Warn("Could not set initial event ID") - } - } - - // If the sync is not finished then a new sync is triggered. - if !loop.store.isSyncFinished() { - loop.store.triggerSync() - } - - more, err := loop.processNextEvent() - if eventProcessedCh != nil { - eventProcessedCh <- struct{}{} - } - if err != nil { - loop.log.WithError(err).Error("Cannot process event, stopping event loop") - // When event loop stops, the only way to start it again is by login. - // It should stop only when user is logged out but even if there is other - // serious error, logout is intended action. - if errLogout := loop.user.Logout(); errLogout != nil { - loop.log. - WithError(errLogout). - Error("Failed to logout user after loop finished with error") - } - return - } - - if more { - go loop.pollNow() - } - } -} - -// isBeforeFirstStart returns whether the initial event ID was already set or not. -func (loop *eventLoop) isBeforeFirstStart() bool { - return loop.currentEventID == "" -} - -// processNextEvent saves only successfully processed `eventID` into cache -// (disk). It will filter out in defer all errors except invalid token error. -// Invalid error will be returned and stop the event loop. -func (loop *eventLoop) processNextEvent() (more bool, err error) { //nolint:funlen - l := loop.log. - WithField("currentEventID", loop.currentEventID). - WithField("pollCounter", loop.pollCounter) - - // We only want to consider invalid tokens as real errors because all other errors might fix themselves eventually - // (e.g. no internet, ulimit reached etc.) - defer func() { - if errors.Cause(err) == pmapi.ErrNoConnection { - l.Warn("Internet unavailable") - err = nil - } - - if err != nil && isFdCloseToULimit() { - l.Warn("Ulimit reached") - loop.listener.Emit(bridgeEvents.RestartBridgeEvent, "") - err = nil - } - - if errors.Cause(err) == pmapi.ErrUpgradeApplication { - l.Warn("Need to upgrade application") - err = nil - } - - if err == nil { - loop.errCounter = 0 - } - - // All errors except ErrUnauthorized (which is not possible to recover from) are ignored. - if err != nil && !pmapi.IsFailedAuth(errors.Cause(err)) && errors.Cause(err) != pmapi.ErrUnauthorized { - l.WithError(err).WithField("errors", loop.errCounter).Error("Error skipped") - loop.errCounter++ - if loop.errCounter == errMaxSentry { - context := map[string]interface{}{ - "EventLoop": map[string]interface{}{ - "EventID": loop.currentEventID, - }, - } - if sentryErr := loop.store.sentryReporter.ReportMessageWithContext("Warning: event loop issues: "+err.Error(), context); sentryErr != nil { - l.WithError(sentryErr).Error("Failed to report error to sentry") - } - } - err = nil - } - }() - - l.Trace("Polling next event") - // Log activity of event loop each 100. poll which means approx. 28 - // lines per day - if loop.pollCounter%100 == 0 { - l.Info("Polling next event") - } - loop.pollCounter++ - - var event *pmapi.Event - if event, err = loop.client().GetEvent(pmapi.ContextWithoutRetry(context.Background()), loop.currentEventID); err != nil { - return false, errors.Wrap(err, "failed to get event") - } - - loop.currentEvent = event - - if event == nil { - return false, errors.New("received empty event") - } - - if err = loop.processEvent(event); err != nil { - return false, errors.Wrap(err, "failed to process event") - } - - if loop.currentEventID != event.EventID { - l.WithField("newID", event.EventID).Info("New event processed") - // In case new event ID cannot be saved to cache, we update it in event loop - // anyway and continue processing new events to prevent the loop from repeatedly - // processing the same event. - // This allows the event loop to continue to function (unless the cache was broken - // and bridge stopped, in which case it will start from the old event ID anyway). - loop.currentEventID = event.EventID - if err = loop.currentEvents.setEventID(loop.user.ID(), event.EventID); err != nil { - return false, errors.Wrap(err, "failed to save event ID to cache") - } - } - - return bool(event.More), err -} - -func (loop *eventLoop) processEvent(event *pmapi.Event) (err error) { - eventLog := loop.log.WithField("event", event.EventID) - eventLog.Debug("Processing event") - - if (event.Refresh & pmapi.EventRefreshMail) != 0 { - eventLog.Info("Processing refresh event") - loop.store.triggerSync() - - context := map[string]interface{}{ - "EventLoop": map[string]interface{}{ - "EventID": loop.currentEventID, - }, - } - if sentryErr := loop.store.sentryReporter.ReportMessageWithContext("Warning: refresh occurred", context); sentryErr != nil { - loop.log.WithError(sentryErr).Error("Failed to report refresh to sentry") - } - - return - } - - if len(event.Addresses) != 0 { - if err = loop.processAddresses(eventLog, event.Addresses); err != nil { - return errors.Wrap(err, "failed to process address events") - } - } - - if len(event.Labels) != 0 { - if err = loop.processLabels(eventLog, event.Labels); err != nil { - return errors.Wrap(err, "failed to process label events") - } - } - - if len(event.Messages) != 0 { - if err = loop.processMessages(eventLog, event.Messages); err != nil { - return errors.Wrap(err, "failed to process message events") - } - } - - if event.User != nil { - loop.user.UpdateSpace(event.User) - loop.listener.Emit(bridgeEvents.UserRefreshEvent, loop.user.ID()) - } - - // One would expect that every event would contain MessageCount as part of - // the event.Messages, but this is apparently not the case. - // MessageCounts are served on an irregular basis, so we should update and - // compare the counts only when we receive them. - if len(event.MessageCounts) != 0 { - if err = loop.processMessageCounts(eventLog, event.MessageCounts); err != nil { - return errors.Wrap(err, "failed to process message count events") - } - } - - if len(event.Notices) != 0 { - loop.processNotices(eventLog, event.Notices) - } - - return err -} - -func (loop *eventLoop) processAddresses(log *logrus.Entry, addressEvents []*pmapi.EventAddress) (err error) { - log.Debug("Processing address change event") - - // Get old addresses for comparisons before updating user. - oldList := loop.client().Addresses() - - if err = loop.user.UpdateUser(context.Background()); err != nil { - if logoutErr := loop.user.Logout(); logoutErr != nil { - log.WithError(logoutErr).Error("Failed to logout user after failed update") - } - return errors.Wrap(err, "failed to update user") - } - - for _, addressEvent := range addressEvents { - switch addressEvent.Action { - case pmapi.EventCreate: - log.WithField("email", addressEvent.Address.Email).Info("Address was created") - loop.listener.Emit(bridgeEvents.AddressChangedEvent, loop.user.GetPrimaryAddress()) - - case pmapi.EventUpdate: - oldAddress := oldList.ByID(addressEvent.ID) - if oldAddress == nil { - log.Warning("Event refers to an address that isn't present") - continue - } - - email := oldAddress.Email - log.WithField("email", email).Info("Address was updated") - if addressEvent.Address.Receive != oldAddress.Receive { - loop.listener.Emit(bridgeEvents.AddressChangedLogoutEvent, email) - } - - case pmapi.EventDelete: - oldAddress := oldList.ByID(addressEvent.ID) - if oldAddress == nil { - log.Warning("Event refers to an address that isn't present") - continue - } - - email := oldAddress.Email - log.WithField("email", email).Info("Address was deleted") - loop.user.CloseConnection(email) - loop.listener.Emit(bridgeEvents.AddressChangedLogoutEvent, email) - case pmapi.EventUpdateFlags: - log.Error("EventUpdateFlags for address event is uknown operation") - } - } - - if err = loop.store.createOrUpdateAddressInfo(loop.client().Addresses()); err != nil { - return errors.Wrap(err, "failed to update address IDs in store") - } - - if err = loop.store.createOrDeleteAddressesEvent(); err != nil { - return errors.Wrap(err, "failed to create/delete store addresses") - } - - return nil -} - -func (loop *eventLoop) processLabels(eventLog *logrus.Entry, labels []*pmapi.EventLabel) error { - eventLog.Debug("Processing label change event") - - for _, eventLabel := range labels { - label := eventLabel.Label - switch eventLabel.Action { - case pmapi.EventCreate, pmapi.EventUpdate: - if err := loop.store.createOrUpdateMailboxEvent(label); err != nil { - return errors.Wrap(err, "failed to create or update label") - } - case pmapi.EventDelete: - if err := loop.store.deleteMailboxEvent(eventLabel.ID); err != nil { - return errors.Wrap(err, "failed to delete label") - } - case pmapi.EventUpdateFlags: - log.Error("EventUpdateFlags for label event is uknown operation") - } - } - - return nil -} - -func (loop *eventLoop) processMessages(eventLog *logrus.Entry, messages []*pmapi.EventMessage) (err error) { //nolint:funlen - eventLog.Debug("Processing message change event") - - for _, message := range messages { - msgLog := eventLog.WithField("msgID", message.ID) - - switch message.Action { - case pmapi.EventCreate: - msgLog.Debug("Processing EventCreate for message") - - if message.Created == nil { - msgLog.Error("Got EventCreate with nil message") - continue - } - - if err = loop.store.createOrUpdateMessageEvent(message.Created); err != nil { - return errors.Wrap(err, "failed to put message into DB") - } - - case pmapi.EventUpdate, pmapi.EventUpdateFlags: - msgLog.Debug("Processing EventUpdate(Flags) for message") - - if message.Updated == nil { - msgLog.Error("Got EventUpdate(Flags) with nil message") - continue - } - - var msg *pmapi.Message - - if msg, err = loop.store.getMessageFromDB(message.ID); err != nil { - if err != ErrNoSuchAPIID { - return errors.Wrap(err, "failed to get message from DB for updating") - } - - msgLog.WithError(err).Warning("Message was not present in DB. Trying fetch...") - - if msg, err = loop.client().GetMessage(context.Background(), message.ID); err != nil { - if pmapi.IsUnprocessableEntity(err) { - msgLog.WithError(err).Warn("Skipping message update because message exists neither in local DB nor on API") - err = nil - continue - } - - return errors.Wrap(err, "failed to get message from API for updating") - } - } - - updateMessage(msgLog, msg, message.Updated) - - loop.removeLabelFromMessageWait(message.Updated.LabelIDsRemoved) - if err = loop.store.createOrUpdateMessageEvent(msg); err != nil { - return errors.Wrap(err, "failed to update message in DB") - } - - case pmapi.EventDelete: - msgLog.Debug("Processing EventDelete for message") - - loop.removeMessageWait(message.ID) - if err = loop.store.deleteMessageEvent(message.ID); err != nil { - return errors.Wrap(err, "failed to delete message from DB") - } - } - } - - return err -} - -// removeMessageWait waits for notifier to be ready to accept delete -// operations for given message. It's no-op if message does not exist. -func (loop *eventLoop) removeMessageWait(msgID string) { - msg, err := loop.store.getMessageFromDB(msgID) - if err != nil { - return - } - loop.removeLabelFromMessageWait(msg.LabelIDs) -} - -// removeLabelFromMessageWait waits for notifier to be ready to accept -// delete operations for given labels. -func (loop *eventLoop) removeLabelFromMessageWait(labelIDs []string) { - if len(labelIDs) == 0 || loop.store.notifier == nil { - return - } - - for { - wasWaiting := false - for _, labelID := range labelIDs { - canDelete, wait := loop.store.notifier.CanDelete(labelID) - if !canDelete { - wasWaiting = true - wait() - } - } - // If we had to wait for some label, we need to check again - // all labels in case something changed in the meantime. - if !wasWaiting { - return - } - } -} - -func updateMessage(msgLog *logrus.Entry, message *pmapi.Message, updates *pmapi.EventMessageUpdated) { //nolint:funlen - msgLog.Debug("Updating message") - - message.Time = updates.Time - - if updates.Subject != nil { - msgLog.WithField("subject", *updates.Subject).Trace("Updating message value") - message.Subject = *updates.Subject - } - - if updates.Sender != nil { - msgLog.WithField("sender", *updates.Sender).Trace("Updating message value") - message.Sender = updates.Sender - } - - if updates.ToList != nil { - msgLog.WithField("toList", *updates.ToList).Trace("Updating message value") - message.ToList = *updates.ToList - } - - if updates.CCList != nil { - msgLog.WithField("ccList", *updates.CCList).Trace("Updating message value") - message.CCList = *updates.CCList - } - - if updates.BCCList != nil { - msgLog.WithField("bccList", *updates.BCCList).Trace("Updating message value") - message.BCCList = *updates.BCCList - } - - if updates.Unread != nil { - msgLog.WithField("unread", *updates.Unread).Trace("Updating message value") - message.Unread = *updates.Unread - } - - if updates.Flags != nil { - msgLog.WithField("flags", *updates.Flags).Trace("Updating message value") - message.Flags = *updates.Flags - } - - if updates.LabelIDs != nil { - msgLog.WithField("labelIDs", updates.LabelIDs).Trace("Updating message value") - message.LabelIDs = updates.LabelIDs - } else { - for _, added := range updates.LabelIDsAdded { - if !message.HasLabelID(added) { - msgLog.WithField("added", added).Trace("Adding label to message") - message.LabelIDs = append(message.LabelIDs, added) - } - } - - labels := []string{} - for _, l := range message.LabelIDs { - removeLabel := false - for _, removed := range updates.LabelIDsRemoved { - if removed == l { - removeLabel = true - break - } - } - if removeLabel { - msgLog.WithField("label", l).Trace("Removing label from message") - } else { - labels = append(labels, l) - } - } - - message.LabelIDs = labels - } -} - -func (loop *eventLoop) processMessageCounts(l *logrus.Entry, messageCounts []*pmapi.MessagesCount) error { - l.WithField("apiCounts", messageCounts).Debug("Processing message count change event") - - isSynced, err := loop.store.isSynced(messageCounts) - if err != nil { - return err - } - if !isSynced { - log.Error("The counts between DB and API are not matching") - } - - return nil -} - -func (loop *eventLoop) processNotices(l *logrus.Entry, notices []string) { - l.Debug("Processing notice change event") - - for _, notice := range notices { - l.Infof("Notice: %q", notice) - for _, address := range loop.user.GetStoreAddresses() { - loop.store.notifyNotice(address, notice) - } - } -} diff --git a/internal/store/event_loop_test.go b/internal/store/event_loop_test.go deleted file mode 100644 index 7db90e71..00000000 --- a/internal/store/event_loop_test.go +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - "net/mail" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -func TestEventLoopProcessMoreEvents(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - // Event expectations need to be defined before calling `newStoreNoEvents` - // to force to use these for this particular test. - // Also, event loop calls ListMessages again and we need to place it after - // calling `newStoreNoEvents` to not break expectations for the first sync. - gomock.InOrder( - // Doesn't matter which IDs are used. - // This test is trying to see whether event loop will immediately process - // next event if there is `More` of them. - m.client.EXPECT().GetEvent(gomock.Any(), "latestEventID").Return(&pmapi.Event{ - EventID: "event50", - More: true, - }, nil), - m.client.EXPECT().GetEvent(gomock.Any(), "event50").Return(&pmapi.Event{ - EventID: "event70", - More: false, - }, nil), - m.client.EXPECT().GetEvent(gomock.Any(), "event70").Return(&pmapi.Event{ - EventID: "event71", - More: false, - }, nil), - ) - m.newStoreNoEvents(t, true) - - // Event loop runs in goroutine started during store creation (newStoreNoEvents). - // Force to run the next event. - m.store.eventLoop.pollNow() - - // More events are processed right away. - require.Eventually(t, func() bool { - return m.store.eventLoop.currentEventID == "event70" - }, time.Second, 10*time.Millisecond) - - // For normal event we need to wait to next polling. - time.Sleep(pollInterval + pollIntervalSpread) - require.Eventually(t, func() bool { - return m.store.eventLoop.currentEventID == "event71" - }, time.Second, 10*time.Millisecond) -} - -func TestEventLoopUpdateMessageFromLoop(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - subject := "old subject" - newSubject := "new subject" - - m.newStoreNoEvents(t, true, &pmapi.Message{ - ID: "msg1", - Subject: subject, - }) - - testEvent(t, m, &pmapi.Event{ - EventID: "event1", - Messages: []*pmapi.EventMessage{{ - EventItem: pmapi.EventItem{ - ID: "msg1", - Action: pmapi.EventUpdate, - }, - Updated: &pmapi.EventMessageUpdated{ - ID: "msg1", - Subject: &newSubject, - }, - }}, - }) - - msg, err := m.store.getMessageFromDB("msg1") - require.NoError(t, err) - require.Equal(t, newSubject, msg.Subject) -} - -func TestEventLoopDeletionNotPaused(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true, &pmapi.Message{ - ID: "msg1", - Subject: "subject", - LabelIDs: []string{"label"}, - }) - - m.changeNotifier.EXPECT().CanDelete("label").Return(true, func() {}) - m.store.SetChangeNotifier(m.changeNotifier) - - testEvent(t, m, &pmapi.Event{ - EventID: "event1", - Messages: []*pmapi.EventMessage{{ - EventItem: pmapi.EventItem{ - ID: "msg1", - Action: pmapi.EventDelete, - }, - }}, - }) - - _, err := m.store.getMessageFromDB("msg1") - require.Error(t, err) -} - -func TestEventLoopDeletionPaused(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true, &pmapi.Message{ - ID: "msg1", - Subject: "subject", - LabelIDs: []string{"label"}, - }) - - delay := 5 * time.Second - - m.changeNotifier.EXPECT().CanDelete("label").Return(false, func() { - time.Sleep(delay) - }) - m.changeNotifier.EXPECT().CanDelete("label").Return(true, func() {}) - m.store.SetChangeNotifier(m.changeNotifier) - - start := time.Now() - - testEvent(t, m, &pmapi.Event{ - EventID: "event1", - Messages: []*pmapi.EventMessage{{ - EventItem: pmapi.EventItem{ - ID: "msg1", - Action: pmapi.EventDelete, - }, - }}, - }) - - _, err := m.store.getMessageFromDB("msg1") - require.Error(t, err) - require.True(t, time.Since(start) > delay) -} - -func testEvent(t *testing.T, m *mocksForStore, event *pmapi.Event) { - eventReceived := make(chan struct{}) - m.client.EXPECT().GetEvent(gomock.Any(), "latestEventID").DoAndReturn(func(_ context.Context, eventID string) (*pmapi.Event, error) { - defer close(eventReceived) - return event, nil - }) - - // Event loop runs in goroutine started during store creation (newStoreNoEvents). - // Force to run the next event. - m.store.eventLoop.pollNow() - - select { - case <-eventReceived: - case <-time.After(5 * time.Second): - require.Fail(t, "latestEventID was not processed") - } -} - -func TestEventLoopUpdateMessage(t *testing.T) { - address1 := &mail.Address{Address: "user1@example.com"} - address2 := &mail.Address{Address: "user2@example.com"} - msg := &pmapi.Message{ - ID: "msg1", - Subject: "old", - Unread: false, - Flags: 10, - Sender: address1, - ToList: []*mail.Address{address2}, - CCList: []*mail.Address{address1}, - BCCList: []*mail.Address{}, - Time: 20, - LabelIDs: []string{"old"}, - } - newMsg := &pmapi.Message{ - ID: "msg1", - Subject: "new", - Unread: true, - Flags: 11, - Sender: address2, - ToList: []*mail.Address{address1}, - CCList: []*mail.Address{address2}, - BCCList: []*mail.Address{address1}, - Time: 21, - LabelIDs: []string{"new"}, - } - - updateMessage(log, msg, &pmapi.EventMessageUpdated{ - ID: "msg1", - Subject: &newMsg.Subject, - Unread: &newMsg.Unread, - Flags: &newMsg.Flags, - Sender: newMsg.Sender, - ToList: &newMsg.ToList, - CCList: &newMsg.CCList, - BCCList: &newMsg.BCCList, - Time: newMsg.Time, - LabelIDs: newMsg.LabelIDs, - }) - - require.Equal(t, newMsg, msg) -} diff --git a/internal/store/events.go b/internal/store/events.go deleted file mode 100644 index 3748f9ce..00000000 --- a/internal/store/events.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "encoding/json" - "os" - "sync" - - "github.com/pkg/errors" -) - -// Events caches the last event IDs for all accounts (there should be only one instance). -type Events struct { - // eventMap is map from userID => key (such as last event) => value (such as event ID). - eventMap map[string]map[string]string - path string - lock *sync.RWMutex -} - -// NewEvents constructs a new event cache at the given path. -func NewEvents(path string) *Events { - return &Events{ - path: path, - lock: &sync.RWMutex{}, - } -} - -func (c *Events) getEventID(userID string) string { - c.lock.Lock() - defer c.lock.Unlock() - - if err := c.loadEvents(); err != nil { - log.WithError(err).Warn("Problem to load store events") - } - - if c.eventMap == nil { - c.eventMap = map[string]map[string]string{} - } - if c.eventMap[userID] == nil { - c.eventMap[userID] = map[string]string{} - } - - return c.eventMap[userID]["events"] -} - -func (c *Events) setEventID(userID, eventID string) error { - c.lock.Lock() - defer c.lock.Unlock() - - if c.eventMap[userID] == nil { - c.eventMap[userID] = map[string]string{} - } - c.eventMap[userID]["events"] = eventID - - return c.saveEvents() -} - -func (c *Events) loadEvents() error { - if c.eventMap != nil { - return nil - } - - f, err := os.Open(c.path) - if err != nil { - return err - } - defer f.Close() //nolint:errcheck,gosec - - return json.NewDecoder(f).Decode(&c.eventMap) -} - -func (c *Events) saveEvents() error { - if c.eventMap == nil { - return errors.New("events: cannot save events: events map is nil") - } - - f, err := os.Create(c.path) - if err != nil { - return err - } - defer f.Close() //nolint:errcheck,gosec - - return json.NewEncoder(f).Encode(c.eventMap) -} - -func (c *Events) clearUserEvents(userID string) error { - c.lock.Lock() - defer c.lock.Unlock() - - if c.eventMap == nil { - log.WithField("user", userID).Warning("Cannot clear user events: event map is nil") - return nil - } - - log.WithField("user", userID).Trace("Removing user events from event loop") - - delete(c.eventMap, userID) - - return c.saveEvents() -} diff --git a/internal/store/mailbox.go b/internal/store/mailbox.go deleted file mode 100644 index 79dd7a95..00000000 --- a/internal/store/mailbox.go +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "encoding/json" - "fmt" - "strings" - "sync/atomic" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/sirupsen/logrus" - bolt "go.etcd.io/bbolt" -) - -// Mailbox is mailbox for specific address and mailbox. -type Mailbox struct { - store *Store - storeAddress *Address - - labelID string - labelPrefix string - labelName string - color string - - log *logrus.Entry - - isDeleting atomic.Value -} - -func newMailbox(storeAddress *Address, labelID, labelPrefix, labelName, color string) (mb *Mailbox, err error) { - err = storeAddress.store.db.Update(func(tx *bolt.Tx) error { - mb, err = txNewMailbox(tx, storeAddress, labelID, labelPrefix, labelName, color) - return err - }) - return -} - -func txNewMailbox(tx *bolt.Tx, storeAddress *Address, labelID, labelPrefix, labelName, color string) (*Mailbox, error) { - l := log.WithField("addrID", storeAddress.addressID).WithField("labelID", labelID) - mb := &Mailbox{ - store: storeAddress.store, - storeAddress: storeAddress, - labelID: labelID, - labelPrefix: labelPrefix, - labelName: labelPrefix + labelName, - color: color, - log: l, - } - mb.isDeleting.Store(false) - - err := initMailboxBucket(tx, mb.getBucketName()) - if err != nil { - l.WithError(err).Error("Could not initialise mailbox buckets") - } - - syncDraftsIfNecssary(tx, mb) - - return mb, err -} - -func syncDraftsIfNecssary(tx *bolt.Tx, mb *Mailbox) { //nolint:funlen - // We didn't support drafts before v1.2.6 and therefore if we now created - // Drafts mailbox we need to check whether counts match (drafts are synced). - // If not, sync them from local metadata without need to do full resync, - // Can be removed with 1.2.7 or later. - if mb.labelID != pmapi.DraftLabel { - return - } - - // If the drafts mailbox total is non-zero, it means it has already been used - // and there is no need to continue. Otherwise, we may need to do an initial sync. - total, _, _, err := mb.txGetCounts(tx) - if err != nil || total != 0 { - return - } - - counts, err := mb.store.txGetOnAPICounts(tx) - if err != nil { - return - } - - foundCounts := false - doSync := false - for _, count := range counts { - if count.LabelID != pmapi.DraftLabel { - continue - } - foundCounts = true - log.WithField("total", total).WithField("total-api", count.TotalOnAPI).Debug("Drafts mailbox created: checking need for sync") - if count.TotalOnAPI == total { - continue - } - doSync = true - break - } - - if !foundCounts { - log.Debug("Drafts mailbox created: missing counts, refreshing") - _ = mb.store.updateCountsFromServer() - } - - if !foundCounts || doSync { - err := tx.Bucket(metadataBucket).ForEach(func(k, v []byte) error { - msg := &pmapi.Message{} - if err := json.Unmarshal(v, msg); err != nil { - return err - } - for _, msgLabelID := range msg.LabelIDs { - if msgLabelID == pmapi.DraftLabel { - log.WithField("id", msg.ID).Trace("Drafts mailbox created: syncing draft locally") - _ = mb.txCreateOrUpdateMessages(tx, []*pmapi.Message{msg}) - break - } - } - return nil - }) - log.WithError(err).Info("Drafts mailbox created: synced localy") - } -} - -func initMailboxBucket(tx *bolt.Tx, bucketName []byte) error { - bucket, err := tx.Bucket(mailboxesBucket).CreateBucketIfNotExists(bucketName) - if err != nil { - return err - } - - if _, err := bucket.CreateBucketIfNotExists(imapIDsBucket); err != nil { - return err - } - if _, err := bucket.CreateBucketIfNotExists(apiIDsBucket); err != nil { - return err - } - if _, err := bucket.CreateBucketIfNotExists(deletedIDsBucket); err != nil { - return err - } - - return nil -} - -// LabelID returns ID of mailbox. -func (storeMailbox *Mailbox) LabelID() string { - return storeMailbox.labelID -} - -// Name returns the name of mailbox. -func (storeMailbox *Mailbox) Name() string { - return storeMailbox.labelName -} - -// Color returns the color of mailbox. -func (storeMailbox *Mailbox) Color() string { - return storeMailbox.color -} - -// UIDValidity returns the current value of structure version. -func (storeMailbox *Mailbox) UIDValidity() uint32 { - return storeMailbox.store.getMailboxesVersion() -} - -// IsFolder returns whether the mailbox is a folder (has "Folders/" prefix). -func (storeMailbox *Mailbox) IsFolder() bool { - return storeMailbox.labelPrefix == UserFoldersPrefix -} - -// IsLabel returns whether the mailbox is a label (has "Labels/" prefix). -func (storeMailbox *Mailbox) IsLabel() bool { - return storeMailbox.labelPrefix == UserLabelsPrefix -} - -// IsSystem returns whether the mailbox is one of the specific system mailboxes (has no prefix). -func (storeMailbox *Mailbox) IsSystem() bool { - return storeMailbox.labelPrefix == "" -} - -// Rename updates the mailbox by calling an API. -// Change has to be propagated to all the same mailboxes in all addresses. -// The propagation is processed by the event loop. -func (storeMailbox *Mailbox) Rename(newName string) error { - if storeMailbox.IsSystem() { - return fmt.Errorf("cannot rename system mailboxes") - } - - if storeMailbox.IsFolder() { - if !strings.HasPrefix(newName, UserFoldersPrefix) { - return fmt.Errorf("cannot rename folder to non-folder") - } - - newName = strings.TrimPrefix(newName, UserFoldersPrefix) - } - - if storeMailbox.IsLabel() { - if !strings.HasPrefix(newName, UserLabelsPrefix) { - return fmt.Errorf("cannot rename label to non-label") - } - - newName = strings.TrimPrefix(newName, UserLabelsPrefix) - } - - return storeMailbox.storeAddress.updateMailbox(storeMailbox.labelID, newName, storeMailbox.color) -} - -// Delete deletes the mailbox by calling an API. -// Deletion has to be propagated to all the same mailboxes in all addresses. -// The propagation is processed by the event loop. -func (storeMailbox *Mailbox) Delete() error { - storeMailbox.isDeleting.Store(true) - return storeMailbox.storeAddress.deleteMailbox(storeMailbox.labelID) -} - -// GetDelimiter returns the path separator. -func (storeMailbox *Mailbox) GetDelimiter() string { - return PathDelimiter -} - -// deleteMailboxEvent deletes the mailbox bucket. -// This is called from the event loop. -func (storeMailbox *Mailbox) deleteMailboxEvent() error { - if !storeMailbox.isDeleting.Load().(bool) { //nolint:forcetypeassert - // Deleting label removes bucket. Any ongoing connection selected - // in such mailbox then might panic because of non-existing bucket. - // Closing connetions prevents that panic but if the connection - // asked for deletion, it should not be closed so it can receive - // successful response. - storeMailbox.store.user.CloseAllConnections() - } - return storeMailbox.db().Update(func(tx *bolt.Tx) error { - return tx.Bucket(mailboxesBucket).DeleteBucket(storeMailbox.getBucketName()) - }) -} - -// txGetIMAPIDsBucket returns the bucket mapping IMAP ID to API ID. -func (storeMailbox *Mailbox) txGetIMAPIDsBucket(tx *bolt.Tx) *bolt.Bucket { - return storeMailbox.txGetBucket(tx).Bucket(imapIDsBucket) -} - -// txGetAPIIDsBucket returns the bucket mapping API ID to IMAP ID. -func (storeMailbox *Mailbox) txGetAPIIDsBucket(tx *bolt.Tx) *bolt.Bucket { - return storeMailbox.txGetBucket(tx).Bucket(apiIDsBucket) -} - -// txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted. -func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket { - return storeMailbox.txGetBucket(tx).Bucket(deletedIDsBucket) -} - -// txGetBucket returns the bucket of mailbox containing mapping buckets. -func (storeMailbox *Mailbox) txGetBucket(tx *bolt.Tx) *bolt.Bucket { - return tx.Bucket(mailboxesBucket).Bucket(storeMailbox.getBucketName()) -} - -func getMailboxBucketName(addressID, labelID string) []byte { - return []byte(addressID + "-" + labelID) -} - -// getBucketName returns the name of mailbox bucket. -func (storeMailbox *Mailbox) getBucketName() []byte { - return getMailboxBucketName(storeMailbox.storeAddress.addressID, storeMailbox.labelID) -} - -// pollNow is a proxy for the store's eventloop's `pollNow()`. -func (storeMailbox *Mailbox) pollNow() { - storeMailbox.store.eventLoop.pollNow() -} - -// api is a proxy for the store's `PMAPIProvider`. -func (storeMailbox *Mailbox) client() pmapi.Client { - return storeMailbox.store.client() -} - -// update is a proxy for the store's db's `Update`. -func (storeMailbox *Mailbox) db() *bolt.DB { - return storeMailbox.store.db -} diff --git a/internal/store/mailbox_counts.go b/internal/store/mailbox_counts.go deleted file mode 100644 index 57ecb4bf..00000000 --- a/internal/store/mailbox_counts.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "bytes" - "encoding/json" - "sort" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - bolt "go.etcd.io/bbolt" -) - -// GetCounts returns numbers of total and unread messages in this mailbox bucket. -func (storeMailbox *Mailbox) GetCounts() (total, unread, unseenSeqNum uint, err error) { - err = storeMailbox.db().View(func(tx *bolt.Tx) error { - total, unread, unseenSeqNum, err = storeMailbox.txGetCounts(tx) - return err - }) - return -} - -func (storeMailbox *Mailbox) txGetCounts(tx *bolt.Tx) (total, unread, unseenSeqNum uint, err error) { - // For total it would be enough to use `bolt.Bucket.Stats().KeyN` but - // we also need to retrieve the count of unread emails therefore we are - // looping all messages in this mailbox by `bolt.Cursor` - metaBucket := tx.Bucket(metadataBucket) - b := storeMailbox.txGetIMAPIDsBucket(tx) - c := b.Cursor() - imapID, apiID := c.First() - for ; imapID != nil; imapID, apiID = c.Next() { - total++ - rawMsg := metaBucket.Get(apiID) - if rawMsg == nil { - return 0, 0, 0, ErrNoSuchAPIID - } - // Do not unmarshal whole JSON to speed up the looping. - // Instead, we assume it will contain JSON int field `Unread` - // where `1` means true (i.e. message is unread) - if bytes.Contains(rawMsg, []byte(`"Unread":1`)) { - if unseenSeqNum == 0 { - unseenSeqNum = total - } - unread++ - } - } - return total, unread, unseenSeqNum, err -} - -type mailboxCounts struct { - LabelID string - LabelName string - Color string - Order int - IsFolder bool - TotalOnAPI uint - UnreadOnAPI uint -} - -func txGetCountsFromBucketOrNew(bkt *bolt.Bucket, labelID string) (*mailboxCounts, error) { - mc := &mailboxCounts{} - if mcJSON := bkt.Get([]byte(labelID)); mcJSON != nil { - if err := json.Unmarshal(mcJSON, mc); err != nil { - return nil, err - } - } - mc.LabelID = labelID // if it was empty before we need to set labelID - - return mc, nil -} - -func (mc *mailboxCounts) txWriteToBucket(bucket *bolt.Bucket) error { - mcJSON, err := json.Marshal(mc) - if err != nil { - return err - } - return bucket.Put([]byte(mc.LabelID), mcJSON) -} - -func getSystemFolders() []*mailboxCounts { - return []*mailboxCounts{ - {pmapi.InboxLabel, "INBOX", "#000", -1000, true, 0, 0}, - {pmapi.SentLabel, "Sent", "#000", -9, true, 0, 0}, - {pmapi.ArchiveLabel, "Archive", "#000", -8, true, 0, 0}, - {pmapi.SpamLabel, "Spam", "#000", -7, true, 0, 0}, - {pmapi.TrashLabel, "Trash", "#000", -6, true, 0, 0}, - {pmapi.AllMailLabel, "All Mail", "#000", -5, true, 0, 0}, - {pmapi.DraftLabel, "Drafts", "#000", -4, true, 0, 0}, - } -} - -// skipThisLabel decides to skip labelIDs that *are* pmapi system labels but *aren't* local system labels -// (i.e. if it's in `pmapi.SystemLabels` but not in `getSystemFolders` then we skip it, otherwise we don't). -func skipThisLabel(labelID string) bool { - switch labelID { - case pmapi.StarredLabel, pmapi.AllSentLabel, pmapi.AllDraftsLabel: - return true - } - return false -} - -func sortByOrder(labels []*pmapi.Label) { - sort.Slice(labels, func(i, j int) bool { - return labels[i].Order < labels[j].Order - }) -} - -func (mc *mailboxCounts) getPMLabel() *pmapi.Label { - return &pmapi.Label{ - ID: mc.LabelID, - Name: mc.LabelName, - Path: mc.LabelName, - Color: mc.Color, - Order: mc.Order, - Type: pmapi.LabelTypeMailBox, - Exclusive: pmapi.Boolean(mc.IsFolder), - } -} - -// createOrUpdateMailboxCountsBuckets will not change the on-API-counts. -func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) error { - // Don't forget about system folders. - // It should set label id, name, color, isFolder, total, unread. - tx := func(tx *bolt.Tx) error { - countsBkt := tx.Bucket(countsBucket) - for _, label := range labels { - // Skipping is probably not necessary. - if skipThisLabel(label.ID) { - continue - } - - // Get current data. - mailbox, err := txGetCountsFromBucketOrNew(countsBkt, label.ID) - if err != nil { - return err - } - - // Update mailbox info, but dont change on-API-counts. - mailbox.LabelName = label.Path - mailbox.Color = label.Color - mailbox.Order = label.Order - mailbox.IsFolder = bool(label.Exclusive) - - // Write. - if err = mailbox.txWriteToBucket(countsBkt); err != nil { - return err - } - } - return nil - } - - return store.db.Update(tx) -} - -func (store *Store) getLabelsFromLocalStorage() ([]*pmapi.Label, error) { - countsOnAPI, err := store.getOnAPICounts() - if err != nil { - return nil, err - } - labels := []*pmapi.Label{} - for _, counts := range countsOnAPI { - labels = append(labels, counts.getPMLabel()) - } - sortByOrder(labels) - - return labels, nil -} - -func (store *Store) getOnAPICounts() (counts []*mailboxCounts, err error) { - err = store.db.View(func(tx *bolt.Tx) error { - counts, err = store.txGetOnAPICounts(tx) - return err - }) - return -} - -func (store *Store) txGetOnAPICounts(tx *bolt.Tx) ([]*mailboxCounts, error) { - counts := []*mailboxCounts{} - c := tx.Bucket(countsBucket).Cursor() - for k, countsB := c.First(); k != nil; k, countsB = c.Next() { - l := store.log.WithField("key", string(k)) - if countsB == nil { - err := errors.New("empty counts in DB") - l.WithError(err).Error("While getting local labels") - return nil, err - } - - mbCounts := &mailboxCounts{} - if err := json.Unmarshal(countsB, mbCounts); err != nil { - l.WithError(err).Error("While unmarshaling local labels") - return nil, err - } - - counts = append(counts, mbCounts) - } - return counts, nil -} - -// createOrUpdateOnAPICounts will change only on-API-counts. -func (store *Store) createOrUpdateOnAPICounts(mailboxCountsOnAPI []*pmapi.MessagesCount) error { - store.log.Debug("Updating API counts") - - tx := func(tx *bolt.Tx) error { - countsBkt := tx.Bucket(countsBucket) - for _, countsOnAPI := range mailboxCountsOnAPI { - if skipThisLabel(countsOnAPI.LabelID) { - continue - } - - // Get current data. - counts, err := txGetCountsFromBucketOrNew(countsBkt, countsOnAPI.LabelID) - if err != nil { - return err - } - - // Update only counts. - counts.TotalOnAPI = uint(countsOnAPI.Total) - counts.UnreadOnAPI = uint(countsOnAPI.Unread) - - if err = counts.txWriteToBucket(countsBkt); err != nil { - return err - } - } - - return nil - } - - return store.db.Update(tx) -} - -func (store *Store) removeMailboxCount(labelID string) error { - err := store.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(countsBucket).Delete([]byte(labelID)) - }) - if err != nil { - store.log.WithError(err). - WithField("labelID", labelID). - Warning("Cannot remove counts") - } - return err -} diff --git a/internal/store/mailbox_counts_test.go b/internal/store/mailbox_counts_test.go deleted file mode 100644 index eb07bb88..00000000 --- a/internal/store/mailbox_counts_test.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "testing" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - a "github.com/stretchr/testify/assert" -) - -func newLabel(order int, id, name string) *pmapi.Label { - return &pmapi.Label{ - ID: id, - Name: name, - Order: order, - } -} - -func TestSortByOrder(t *testing.T) { - want := []*pmapi.Label{ - newLabel(-1000, pmapi.InboxLabel, "INBOX"), - newLabel(-5, pmapi.SentLabel, "Sent"), - newLabel(-4, pmapi.ArchiveLabel, "Archive"), - newLabel(-3, pmapi.SpamLabel, "Spam"), - newLabel(-2, pmapi.TrashLabel, "Trash"), - newLabel(-1, pmapi.AllMailLabel, "All Mail"), - newLabel(100, "labelID1", "custom_label"), - newLabel(1000, "folderID1", "custom_folder"), - } - labels := []*pmapi.Label{ - want[6], - want[4], - want[3], - want[7], - want[5], - want[0], - want[2], - want[1], - } - - sortByOrder(labels) - a.Equal(t, want, labels) -} - -func TestMailboxNames(t *testing.T) { - want := map[string]string{ - pmapi.InboxLabel: "INBOX", - pmapi.SentLabel: "Sent", - pmapi.ArchiveLabel: "Archive", - pmapi.SpamLabel: "Spam", - pmapi.TrashLabel: "Trash", - pmapi.AllMailLabel: "All Mail", - pmapi.DraftLabel: "Drafts", - "labelID1": "Labels/Label1", - "folderID1": "Folders/Folder1", - } - - foldersAndLabels := []*pmapi.Label{ - newLabel(100, "labelID1", "Label1"), - newLabel(1000, "folderID1", "Folder1"), - } - foldersAndLabels[1].Exclusive = true - - for _, counts := range getSystemFolders() { - foldersAndLabels = append(foldersAndLabels, counts.getPMLabel()) - } - - got := map[string]string{} - for _, m := range foldersAndLabels { - got[m.ID] = getLabelPrefix(m) + m.Name - } - a.Equal(t, want, got) -} - -func TestAddSystemLabels(t *testing.T) {} - -func checkCounts(t testing.TB, wantCounts []*pmapi.MessagesCount, haveStore *Store) { - nSystemFolders := 7 - haveCounts, err := haveStore.getOnAPICounts() - a.NoError(t, err) - a.Len(t, haveCounts, len(wantCounts)+nSystemFolders) - for iWant, wantCount := range wantCounts { - iHave := iWant + nSystemFolders - haveCount := haveCounts[iHave] - a.Equal(t, wantCount.LabelID, haveCount.LabelID, "iHave:%d\niWant:%d\nHave:%v\nWant:%v", iHave, iWant, haveCount, wantCount) - a.Equal(t, wantCount.Total, int(haveCount.TotalOnAPI), "iHave:%d\niWant:%d\nHave:%v\nWant:%v", iHave, iWant, haveCount, wantCount) - a.Equal(t, wantCount.Unread, int(haveCount.UnreadOnAPI), "iHave:%d\niWant:%d\nHave:%v\nWant:%v", iHave, iWant, haveCount, wantCount) - } -} - -func TestMailboxCountRemove(t *testing.T) { - m, clear := initMocks(t) - defer clear() - m.newStoreNoEvents(t, true) - - testCounts := []*pmapi.MessagesCount{ - {LabelID: "label1", Total: 100, Unread: 0}, - {LabelID: "label2", Total: 100, Unread: 30}, - {LabelID: "label4", Total: 100, Unread: 100}, - } - a.NoError(t, m.store.createOrUpdateOnAPICounts(testCounts)) - - a.NoError(t, m.store.removeMailboxCount("not existing")) - checkCounts(t, testCounts, m.store) - - var pop *pmapi.MessagesCount - pop, testCounts = testCounts[2], testCounts[0:2] - a.NoError(t, m.store.removeMailboxCount(pop.LabelID)) - checkCounts(t, testCounts, m.store) -} diff --git a/internal/store/mailbox_ids.go b/internal/store/mailbox_ids.go deleted file mode 100644 index 658d68dd..00000000 --- a/internal/store/mailbox_ids.go +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "bytes" - "math" - "net/mail" - "regexp" - "strings" - - "github.com/ProtonMail/proton-bridge/v2/internal/imap/uidplus" - "github.com/pkg/errors" - bolt "go.etcd.io/bbolt" -) - -// GetAPIIDsFromUIDRange returns API IDs by IMAP UID range. -// -// API IDs are the long base64 strings that the API uses to identify messages. -// UIDs are unique increasing integers that must be unique within a mailbox. -func (storeMailbox *Mailbox) GetAPIIDsFromUIDRange(start, stop uint32) (apiIDs []string, err error) { - err = storeMailbox.db().View(func(tx *bolt.Tx) error { - b := storeMailbox.txGetIMAPIDsBucket(tx) - c := b.Cursor() - - // GODT-1153 If the mailbox is empty we should reply BAD to client. - if uid, _ := c.Last(); uid == nil { - return nil - } - - // If the start range is a wildcard, the range can only refer to the last message in the mailbox. - if start == 0 { - _, apiID := c.Last() - apiIDs = append(apiIDs, string(apiID)) - return nil - } - - // Resolve the stop value to be the final UID in the mailbox. - if stop == 0 { - stop = storeMailbox.txGetFinalUID(b) - } - - // After resolving the stop value, it might be less than start so we sort it. - if start > stop { - start, stop = stop, start - } - - startb := itob(start) - stopb := itob(stop) - - for k, v := c.Seek(startb); k != nil && bytes.Compare(k, stopb) <= 0; k, v = c.Next() { - apiIDs = append(apiIDs, string(v)) - } - - return nil - }) - - return apiIDs, err -} - -// GetAPIIDsFromSequenceRange returns API IDs by IMAP sequence number range. -func (storeMailbox *Mailbox) GetAPIIDsFromSequenceRange(start, stop uint32) (apiIDs []string, err error) { - err = storeMailbox.db().View(func(tx *bolt.Tx) error { - b := storeMailbox.txGetIMAPIDsBucket(tx) - c := b.Cursor() - - // GODT-1153 If the mailbox is empty we should reply BAD to client. - if uid, _ := c.Last(); uid == nil { - return nil - } - - // If the start range is a wildcard, the range can only refer to the last message in the mailbox. - if start == 0 { - _, apiID := c.Last() - apiIDs = append(apiIDs, string(apiID)) - return nil - } - - var i uint32 - - for k, v := c.First(); k != nil; k, v = c.Next() { - i++ - - if i < start { - continue - } - - if stop > 0 && i > stop { - break - } - - apiIDs = append(apiIDs, string(v)) - } - - if stop == 0 && len(apiIDs) == 0 { - if _, apiID := c.Last(); len(apiID) > 0 { - apiIDs = append(apiIDs, string(apiID)) - } - } - - return nil - }) - - return apiIDs, err -} - -// GetLatestAPIID returns the latest message API ID which still exists. -// Info: not the latest IMAP UID which can be already removed. -func (storeMailbox *Mailbox) GetLatestAPIID() (apiID string, err error) { - err = storeMailbox.db().View(func(tx *bolt.Tx) error { - c := storeMailbox.txGetAPIIDsBucket(tx).Cursor() - lastAPIID, _ := c.Last() - apiID = string(lastAPIID) - if apiID == "" { - return errors.New("cannot get latest API ID: empty mailbox") - } - return nil - }) - return -} - -// GetNextUID returns the next IMAP UID. -func (storeMailbox *Mailbox) GetNextUID() (uid uint32, err error) { - err = storeMailbox.db().View(func(tx *bolt.Tx) error { - b := storeMailbox.txGetIMAPIDsBucket(tx) - uid, err = storeMailbox.txGetNextUID(b, false) - return err - }) - return -} - -func (storeMailbox *Mailbox) txGetNextUID(imapIDBucket *bolt.Bucket, write bool) (uint32, error) { - var uid uint64 - var err error - if write { - uid, err = imapIDBucket.NextSequence() - if err != nil { - return 0, err - } - } else { - uid = imapIDBucket.Sequence() + 1 - } - if math.MaxUint32 <= uid { - return 0, errors.New("too large sequence number") - } - return uint32(uid), nil -} - -// getUID returns IMAP UID in this mailbox for message ID. -func (storeMailbox *Mailbox) getUID(apiID string) (uid uint32, err error) { - err = storeMailbox.db().View(func(tx *bolt.Tx) error { - uid, err = storeMailbox.txGetUID(tx, apiID) - return err - }) - return -} - -func (storeMailbox *Mailbox) txGetUID(tx *bolt.Tx, apiID string) (uint32, error) { - return storeMailbox.txGetUIDFromBucket(storeMailbox.txGetAPIIDsBucket(tx), apiID) -} - -// txGetUIDFromBucket expects pointer to API bucket. -func (storeMailbox *Mailbox) txGetUIDFromBucket(b *bolt.Bucket, apiID string) (uint32, error) { - v := b.Get([]byte(apiID)) - if v == nil { - return 0, ErrNoSuchAPIID - } - return btoi(v), nil -} - -// GetDeletedAPIIDs returns API IDs in this mailbox for message ID. -func (storeMailbox *Mailbox) GetDeletedAPIIDs() (apiIDs []string, err error) { - err = storeMailbox.db().Update(func(tx *bolt.Tx) error { - b := storeMailbox.txGetDeletedIDsBucket(tx) - c := b.Cursor() - for k, _ := c.First(); k != nil; k, _ = c.Next() { - apiIDs = append(apiIDs, string(k)) - } - return nil - }) - return -} - -// getSequenceNumber returns IMAP sequence number in the mailbox for the message with the given API ID `apiID`. -func (storeMailbox *Mailbox) getSequenceNumber(apiID string) (seqNum uint32, err error) { - err = storeMailbox.db().View(func(tx *bolt.Tx) error { - b := storeMailbox.txGetIMAPIDsBucket(tx) - uid, err := storeMailbox.txGetUID(tx, apiID) - if err != nil { - return err - } - seqNum, err = storeMailbox.txGetSequenceNumberOfUID(b, itob(uid)) - return err - }) - return -} - -// txGetSequenceNumberOfUID returns the IMAP sequence number of the message -// with the given IMAP UID bytes `uidb`. -// -// NOTE: The `bolt.Cursor.Next()` loops in order of ascending key bytes. The -// IMAP UID bucket is ordered by increasing UID because it's using BigEndian to -// encode uint into byte. Hence the sequence number (IMAP ID) corresponds to -// position of uid key in this order. -func (storeMailbox *Mailbox) txGetSequenceNumberOfUID(bucket *bolt.Bucket, uidb []byte) (uint32, error) { - seqNum := uint32(0) - c := bucket.Cursor() - - // Speed up for the case of last message. This is always true for - // adding new message. It will return number of keys in bucket because - // sequence number starts with 1. - // We cannot use bucket.Stats() for that--it doesn't work in the same - // transaction because stats are updated when transaction is committed. - // But we can at least optimise to not do equal for all keys. - lastKey, _ := c.Last() - isLast := bytes.Equal(lastKey, uidb) - - for k, _ := c.First(); k != nil; k, _ = c.Next() { - seqNum++ // Sequence number starts at 1. - if isLast { - continue - } - if bytes.Equal(k, uidb) { - return seqNum, nil - } - } - - if isLast { - return seqNum, nil - } - - return 0, ErrNoSuchUID -} - -// GetUIDList returns UID list corresponding to messageIDs in a requested order. -func (storeMailbox *Mailbox) GetUIDList(apiIDs []string) *uidplus.OrderedSeq { - seqSet := &uidplus.OrderedSeq{} - _ = storeMailbox.db().View(func(tx *bolt.Tx) error { - b := storeMailbox.txGetAPIIDsBucket(tx) - for _, apiID := range apiIDs { - v := b.Get([]byte(apiID)) - if v == nil { - storeMailbox.log. - WithField("msgID", apiID). - Warn("Cannot find UID") - continue - } - - seqSet.Add(btoi(v)) - } - return nil - }) - return seqSet -} - -// GetUIDByHeader returns UID of message existing in mailbox or zero if no match found. -func (storeMailbox *Mailbox) GetUIDByHeader(header *mail.Header) (foundUID uint32) { - if header == nil { - return uint32(0) - } - - // Message-Id in appended-after-send mail is processed as ExternalID - // in PM message. Message-Id in normal copy/move will be the PM internal ID. - messageID := header.Get("Message-Id") - - // There is nothing to find, when no Message-Id given. - if messageID == "" { - return uint32(0) - } - - // The most often situation is that message is APPENDed after it was sent so the - // Message-ID will be reflected by ExternalID in API message meta-data. - externalID := strings.Trim(messageID, "<> ") // remove '<>' to improve match - matchExternalID := regexp.MustCompile(`"ExternalID":"` + - ` *(\\u003c)? *` + // \u003c is equivalent to `<` - regexp.QuoteMeta(externalID) + - ` *(\\u003e)? *` + // \u0033 is equivalent to `>` - `"`, - ) - - // It is possible that client will try to COPY existing message to Sent - // using APPEND command. In that case the Message-Id from header will - // be internal message ID and we need to check whether it's already there. - matchInternalID := bytes.Split([]byte(externalID), []byte("@"))[0] - - _ = storeMailbox.db().View(func(tx *bolt.Tx) error { - metaBucket := tx.Bucket(metadataBucket) - b := storeMailbox.txGetIMAPIDsBucket(tx) - c := b.Cursor() - imapID, apiID := c.Last() - for ; imapID != nil; imapID, apiID = c.Prev() { - rawMeta := metaBucket.Get(apiID) - if rawMeta == nil { - storeMailbox.log. - WithField("IMAP-UID", imapID). - WithField("API-ID", apiID). - Warn("Cannot find meta-data while searching for externalID") - continue - } - - if !matchExternalID.Match(rawMeta) && !bytes.Equal(apiID, matchInternalID) { - continue - } - - foundUID = btoi(imapID) - return nil - } - return nil - }) - - return foundUID -} - -func (storeMailbox *Mailbox) txGetFinalUID(b *bolt.Bucket) uint32 { - uid, _ := b.Cursor().Last() - - if uid == nil { - // This happened most probably due to empty mailbox and whole - // store needs to be re-initialize in order to fix it. - panic(errors.New("cannot get final UID")) - } - - return btoi(uid) -} diff --git a/internal/store/mailbox_ids_test.go b/internal/store/mailbox_ids_test.go deleted file mode 100644 index 0a3a5eb5..00000000 --- a/internal/store/mailbox_ids_test.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "net/mail" - "testing" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - a "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type wantID struct { - appID string - uid int -} - -func TestGetSequenceNumberAndGetUID(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true) - - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) - insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel}) - insertMessage(t, m, "msg3", "Test message 3", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) - insertMessage(t, m, "msg4", "Test message 4", addrID1, false, []string{pmapi.AllMailLabel}) - - checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"}) - - checkMailboxMessageIDs(t, m, pmapi.InboxLabel, []wantID{{"msg1", 1}, {"msg3", 2}}) - checkMailboxMessageIDs(t, m, pmapi.ArchiveLabel, []wantID{{"msg2", 1}}) - checkMailboxMessageIDs(t, m, pmapi.SpamLabel, []wantID(nil)) - checkMailboxMessageIDs(t, m, pmapi.AllMailLabel, []wantID{{"msg1", 1}, {"msg2", 2}, {"msg3", 3}, {"msg4", 4}}) -} - -// checkMailboxMessageIDs checks that the mailbox contains all API IDs with correct sequence numbers and UIDs. -// wantIDs is map from IMAP UID to API ID. Sequence number is detected automatically by order of the ID in the map. -func checkMailboxMessageIDs(t *testing.T, m *mocksForStore, mailboxLabel string, wantIDs []wantID) { - storeAddress := m.store.addresses[addrID1] - storeMailbox := storeAddress.mailboxes[mailboxLabel] - - ids, err := storeMailbox.GetAPIIDsFromSequenceRange(1, uint32(len(wantIDs))) - require.Nil(t, err) - - idx := 0 - for _, wantID := range wantIDs { - id := ids[idx] - require.Equal(t, wantID.appID, id, "Got IDs: %+v", ids) - - uid, err := storeMailbox.getUID(wantID.appID) - require.Nil(t, err) - a.Equal(t, uint32(wantID.uid), uid) - - seqNum, err := storeMailbox.getSequenceNumber(wantID.appID) - require.Nil(t, err) - a.Equal(t, uint32(idx+1), seqNum) - - idx++ - } -} - -func TestGetUIDByHeader(t *testing.T) { //nolint:funlen - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true) - - tstMsg := getTestMessage("msg1", "Without external ID", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel}) - require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) - - tstMsg = getTestMessage("msg2", "External ID with spaces", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel}) - tstMsg.ExternalID = " externalID-non-pm-com " - require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) - - tstMsg = getTestMessage("msg3", "External ID with <>", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel}) - tstMsg.ExternalID = "" - tstMsg.Header = mail.Header{"References": []string{"wrongID", "externalID-non-pm-com", "msg2"}} - require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) - - // Not sure if this is a real-world scenario but we should be able to address this properly. - tstMsg = getTestMessage("msg4", "External ID with <> and spaces and special characters", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel}) - tstMsg.ExternalID = " < external.()+*[]ID@another.pm.me > " - require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) - - testDataUIDByHeader := []struct { - header *mail.Header - wantID uint32 - }{ - { - &mail.Header{"Message-Id": []string{"wrongID"}}, - 0, - }, - { - &mail.Header{"Message-Id": []string{"ext"}}, - 0, - }, - { - &mail.Header{"Message-Id": []string{"externalID"}}, - 0, - }, - { - &mail.Header{"Message-Id": []string{"msg1"}}, - 1, - }, - { - &mail.Header{"Message-Id": []string{""}}, - 3, - }, - { - &mail.Header{"Message-Id": []string{""}}, - 2, - }, - { - &mail.Header{"Message-Id": []string{"externalID@pm.me"}}, - 3, - }, - { - &mail.Header{"Message-Id": []string{"external.()+*[]ID@another.pm.me"}}, - 4, - }, - } - - storeAddress := m.store.addresses[addrID1] - storeMailbox := storeAddress.mailboxes[pmapi.SentLabel] - - for _, td := range testDataUIDByHeader { - haveID := storeMailbox.GetUIDByHeader(td.header) - a.Equal(t, td.wantID, haveID, "testing header: %v", td.header) - } -} diff --git a/internal/store/mailbox_message.go b/internal/store/mailbox_message.go deleted file mode 100644 index 1c69ddde..00000000 --- a/internal/store/mailbox_message.go +++ /dev/null @@ -1,555 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - bolt "go.etcd.io/bbolt" -) - -// ErrAllMailOpNotAllowed is error user when user tries to do unsupported -// operation on All Mail folder. -var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder") - -// GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage` -// tied to this mailbox. -func (storeMailbox *Mailbox) GetMessage(apiID string) (*Message, error) { - msg, err := storeMailbox.store.getMessageFromDB(apiID) - if err != nil { - return nil, err - } - return newStoreMessage(storeMailbox, msg), nil -} - -// FetchMessage fetches the message with the given `apiID`, stores it in the database, and returns a new store message -// wrapping it. -func (storeMailbox *Mailbox) FetchMessage(apiID string) (*Message, error) { - msg, err := storeMailbox.client().GetMessage(exposeContextForIMAP(), apiID) - if err != nil { - return nil, err - } - return newStoreMessage(storeMailbox, msg), nil -} - -func (storeMailbox *Mailbox) ImportMessage(enc []byte, seen bool, labelIDs []string, flags, time int64) (string, error) { - defer storeMailbox.pollNow() - - if storeMailbox.labelID != pmapi.AllMailLabel { - labelIDs = append(labelIDs, storeMailbox.labelID) - } - - importReqs := &pmapi.ImportMsgReq{ - Metadata: &pmapi.ImportMetadata{ - AddressID: storeMailbox.storeAddress.addressID, - Unread: pmapi.Boolean(!seen), - Flags: flags, - Time: time, - LabelIDs: labelIDs, - }, - Message: append(enc, "\r\n"...), - } - - res, err := storeMailbox.client().Import(exposeContextForIMAP(), pmapi.ImportMsgReqs{importReqs}) - if err != nil { - return "", err - } - - if len(res) == 0 { - return "", errors.New("no import response") - } - - return res[0].MessageID, res[0].Error -} - -// LabelMessages adds the label by calling an API. -// It has to be propagated to all the same messages in all mailboxes. -// The propagation is processed by the event loop. -func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error { - log.WithFields(logrus.Fields{ - "messages": apiIDs, - "label": storeMailbox.labelID, - "mailbox": storeMailbox.Name, - }).Trace("Labeling messages") - // Edge case is want to untrash message by drag&drop to AllMail (to not - // have it in trash but to not delete message forever). IMAP move would - // work okay but some clients might use COPY&EXPUNGE or APPEND&EXPUNGE. - // In this case COPY or APPEND is noop because the message is already - // in All mail. The consequent EXPUNGE would delete message forever. - if storeMailbox.labelID == pmapi.AllMailLabel { - return ErrAllMailOpNotAllowed - } - defer storeMailbox.pollNow() - return storeMailbox.client().LabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID) -} - -// UnlabelMessages removes the label by calling an API. -// It has to be propagated to all the same messages in all mailboxes. -// The propagation is processed by the event loop. -func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error { - storeMailbox.log.WithField("messages", apiIDs). - Trace("Unlabeling messages") - if storeMailbox.labelID == pmapi.AllMailLabel { - return ErrAllMailOpNotAllowed - } - defer storeMailbox.pollNow() - return storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID) -} - -// MarkMessagesRead marks the message read by calling an API. -// It has to be propagated to metadata mailbox which is done by the event loop. -func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error { - log.WithFields(logrus.Fields{ - "messages": apiIDs, - "label": storeMailbox.labelID, - "mailbox": storeMailbox.Name, - }).Trace("Marking messages as read") - defer storeMailbox.pollNow() - - // Before deleting a message, TB sets \Seen flag which causes an event update - // and thus a refresh of the message by deleting and creating it again. - // TB does not notice this and happily continues with next command to move - // the message to the Trash but the message does not exist anymore. - // Therefore we do not issue API update if the message is already read. - ids := []string{} - for _, apiID := range apiIDs { - if message, _ := storeMailbox.store.getMessageFromDB(apiID); message == nil || message.Unread { - ids = append(ids, apiID) - } - } - if len(ids) == 0 { - return nil - } - return storeMailbox.client().MarkMessagesRead(exposeContextForIMAP(), ids) -} - -// MarkMessagesUnread marks the message unread by calling an API. -// It has to be propagated to metadata mailbox which is done by the event loop. -func (storeMailbox *Mailbox) MarkMessagesUnread(apiIDs []string) error { - log.WithFields(logrus.Fields{ - "messages": apiIDs, - "label": storeMailbox.labelID, - "mailbox": storeMailbox.Name, - }).Trace("Marking messages as unread") - defer storeMailbox.pollNow() - return storeMailbox.client().MarkMessagesUnread(exposeContextForIMAP(), apiIDs) -} - -// MarkMessagesStarred adds the Starred label by calling an API. -// It has to be propagated to all the same messages in all mailboxes. -// The propagation is processed by the event loop. -func (storeMailbox *Mailbox) MarkMessagesStarred(apiIDs []string) error { - log.WithFields(logrus.Fields{ - "messages": apiIDs, - "label": storeMailbox.labelID, - "mailbox": storeMailbox.Name, - }).Trace("Marking messages as starred") - defer storeMailbox.pollNow() - return storeMailbox.client().LabelMessages(exposeContextForIMAP(), apiIDs, pmapi.StarredLabel) -} - -// MarkMessagesUnstarred removes the Starred label by calling an API. -// It has to be propagated to all the same messages in all mailboxes. -// The propagation is processed by the event loop. -func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error { - log.WithFields(logrus.Fields{ - "messages": apiIDs, - "label": storeMailbox.labelID, - "mailbox": storeMailbox.Name, - }).Trace("Marking messages as unstarred") - defer storeMailbox.pollNow() - return storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, pmapi.StarredLabel) -} - -// MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API -// until RemoveDeleted is called. -func (storeMailbox *Mailbox) MarkMessagesDeleted(apiIDs []string) error { - log.WithFields(logrus.Fields{ - "messages": apiIDs, - "label": storeMailbox.labelID, - "mailbox": storeMailbox.Name, - }).Trace("Marking messages as deleted") - if storeMailbox.labelID == pmapi.AllMailLabel { - return ErrAllMailOpNotAllowed - } - return storeMailbox.store.db.Update(func(tx *bolt.Tx) error { - return storeMailbox.txMarkMessagesAsDeleted(tx, apiIDs, true) - }) -} - -// MarkMessagesUndeleted removes local flag \Deleted. This is not propagated to -// API. -func (storeMailbox *Mailbox) MarkMessagesUndeleted(apiIDs []string) error { - log.WithFields(logrus.Fields{ - "messages": apiIDs, - "label": storeMailbox.labelID, - "mailbox": storeMailbox.Name, - }).Trace("Marking messages as undeleted") - if storeMailbox.labelID == pmapi.AllMailLabel { - return ErrAllMailOpNotAllowed - } - return storeMailbox.store.db.Update(func(tx *bolt.Tx) error { - return storeMailbox.txMarkMessagesAsDeleted(tx, apiIDs, false) - }) -} - -// RemoveDeleted sends request to API to remove message from mailbox. -// If the mailbox is All Mail or All Sent, it does nothing. -// If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted. -// In all other cases the message is only removed from the mailbox. -// If nil is passed, all messages with \Deleted flag are removed. -// In other cases only messages with \Deleted flag and included in the passed list. -func (storeMailbox *Mailbox) RemoveDeleted(apiIDs []string) error { - storeMailbox.log.Trace("Deleting messages") - - deletedAPIIDs, err := storeMailbox.GetDeletedAPIIDs() - if err != nil { - return err - } - - if apiIDs == nil { - apiIDs = deletedAPIIDs - } else { - filteredAPIIDs := []string{} - for _, apiID := range apiIDs { - found := false - for _, deletedAPIID := range deletedAPIIDs { - if apiID == deletedAPIID { - found = true - break - } - } - if found { - filteredAPIIDs = append(filteredAPIIDs, apiID) - } - } - apiIDs = filteredAPIIDs - } - - if len(apiIDs) == 0 { - storeMailbox.log.Debug("List to expunge is empty") - return nil - } - - defer storeMailbox.pollNow() - - switch storeMailbox.labelID { - case pmapi.AllMailLabel, pmapi.AllSentLabel: - break - case pmapi.TrashLabel, pmapi.SpamLabel: - if err := storeMailbox.deleteFromTrashOrSpam(apiIDs); err != nil { - return err - } - case pmapi.DraftLabel: - storeMailbox.log.WithField("ids", apiIDs).Warn("Deleting drafts") - if err := storeMailbox.client().DeleteMessages(exposeContextForIMAP(), apiIDs); err != nil { - return err - } - default: - if err := storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID); err != nil { - return err - } - } - return nil -} - -// deleteFromTrashOrSpam will remove messages from API forever. If messages -// still has some custom label the message will not be deleted. Instead it will -// be removed from Trash or Spam. -func (storeMailbox *Mailbox) deleteFromTrashOrSpam(apiIDs []string) error { - l := storeMailbox.log.WithField("messages", apiIDs) - l.Trace("Deleting messages from trash") - - messageIDsToDelete := []string{} - messageIDsToUnlabel := []string{} - for _, apiID := range apiIDs { - msg, err := storeMailbox.store.getMessageFromDB(apiID) - if err != nil { - return err - } - - otherLabels := false - // If the message has any custom label, we don't want to delete it, only remove trash/spam label. - for _, label := range msg.LabelIDs { - if label != pmapi.SpamLabel && label != pmapi.TrashLabel && label != pmapi.AllMailLabel && label != pmapi.AllSentLabel && label != pmapi.DraftLabel && label != pmapi.AllDraftsLabel { - otherLabels = true - break - } - } - - if otherLabels { - messageIDsToUnlabel = append(messageIDsToUnlabel, apiID) - } else { - messageIDsToDelete = append(messageIDsToDelete, apiID) - } - } - if len(messageIDsToUnlabel) > 0 { - if err := storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), messageIDsToUnlabel, storeMailbox.labelID); err != nil { - l.WithError(err).Warning("Cannot unlabel before deleting") - } - } - if len(messageIDsToDelete) > 0 { - storeMailbox.log.WithField("ids", messageIDsToDelete).Warn("Deleting messages") - if err := storeMailbox.client().DeleteMessages(exposeContextForIMAP(), messageIDsToDelete); err != nil { - return err - } - } - - return nil -} - -func (storeMailbox *Mailbox) txSkipAndRemoveFromMailbox(tx *bolt.Tx, msg *pmapi.Message) (skipAndRemove bool) { - defer func() { - if skipAndRemove { - if err := storeMailbox.txDeleteMessage(tx, msg.ID); err != nil { - storeMailbox.log.WithError(err).Error("Cannot remove message") - } - } - }() - - mode, err := storeMailbox.store.getAddressMode() - if err != nil { - log.WithError(err).Error("Could not determine address mode") - return - } - - skipAndRemove = true - - // If it's split mode and it shouldn't be under this address, it should be skipped and removed. - if mode == splitMode && storeMailbox.storeAddress.addressID != msg.AddressID { - return - } - - // If the message belongs in this mailbox, don't skip/remove it. - for _, labelID := range msg.LabelIDs { - if labelID == storeMailbox.labelID { - skipAndRemove = false - return - } - } - - return skipAndRemove -} - -// txCreateOrUpdateMessages will delete, create or update message from mailbox. -func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi.Message) error { //nolint:funlen - shouldSendMailboxUpdate := false - - // Buckets are not initialized right away because it's a heavy operation. - // The best option is to get the same bucket only once and only when needed. - var apiBucket, imapBucket, deletedBucket *bolt.Bucket - - // Collect updates to send them later, after possibly sending the status/EXISTS update. - updates := make([]func(), 0, len(msgs)) - - for _, msg := range msgs { - if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) { - continue - } - - // Update message. - if apiBucket == nil { - apiBucket = storeMailbox.txGetAPIIDsBucket(tx) - } - - // Draft bodies can change and bodies are not re-fetched by IMAP clients. - // Every change has to be a new message; we need to delete the old one and always recreate it. - if msg.IsDraft() { - if err := storeMailbox.txDeleteMessage(tx, msg.ID); err != nil { - return errors.Wrap(err, "cannot delete old draft") - } - } else { - uidb := apiBucket.Get([]byte(msg.ID)) - if uidb != nil { - if imapBucket == nil { - imapBucket = storeMailbox.txGetIMAPIDsBucket(tx) - } - seqNum, seqErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb) - if deletedBucket == nil { - deletedBucket = storeMailbox.txGetDeletedIDsBucket(tx) - } - isMarkedAsDeleted := deletedBucket.Get([]byte(msg.ID)) != nil - if seqErr == nil { - storeMailbox.store.notifyUpdateMessage( - storeMailbox.storeAddress.address, - storeMailbox.labelName, - btoi(uidb), - seqNum, - msg, - isMarkedAsDeleted, - ) - } - continue - } - } - - // Create a new message. - if imapBucket == nil { - imapBucket = storeMailbox.txGetIMAPIDsBucket(tx) - } - uid, err := storeMailbox.txGetNextUID(imapBucket, true) - if err != nil { - return errors.Wrap(err, "cannot generate new UID") - } - uidb := itob(uid) - - if err = imapBucket.Put(uidb, []byte(msg.ID)); err != nil { - return errors.Wrap(err, "cannot add to IMAP bucket") - } - if err = apiBucket.Put([]byte(msg.ID), uidb); err != nil { - return errors.Wrap(err, "cannot add to API bucket") - } - - seqNum, err := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb) - if err != nil { - return errors.Wrap(err, "cannot get sequence number from UID") - } - - updates = append(updates, func() { - storeMailbox.store.notifyUpdateMessage( - storeMailbox.storeAddress.address, - storeMailbox.labelName, - uid, - seqNum, - msg, - false, // new message is never marked as deleted - ) - }) - - shouldSendMailboxUpdate = true - } - - if shouldSendMailboxUpdate { - if err := storeMailbox.txMailboxStatusUpdate(tx); err != nil { - return err - } - } - - for _, update := range updates { - update() - } - - return nil -} - -// txDeleteMessage deletes the message from the mailbox bucket. -// and issues message delete and mailbox update changes to updates channel. -func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error { - apiBucket := storeMailbox.txGetAPIIDsBucket(tx) - apiIDb := []byte(apiID) - uidb := apiBucket.Get(apiIDb) - if uidb == nil { - return nil - } - - imapBucket := storeMailbox.txGetIMAPIDsBucket(tx) - deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx) - - seqNum, seqNumErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb) - if seqNumErr != nil { - storeMailbox.log.WithField("apiID", apiID).WithError(seqNumErr).Warn("Cannot get seqNum of deleting message") - } - - if err := imapBucket.Delete(uidb); err != nil { - return errors.Wrap(err, "cannot delete from IMAP bucket") - } - - if err := apiBucket.Delete(apiIDb); err != nil { - return errors.Wrap(err, "cannot delete from API bucket") - } - - if err := deletedBucket.Delete(apiIDb); err != nil { - return errors.Wrap(err, "cannot delete from mark-as-deleted bucket") - } - - if seqNumErr == nil { - storeMailbox.store.notifyDeleteMessage( - storeMailbox.storeAddress.address, - storeMailbox.labelName, - seqNum, - ) - // Outlook for Mac has problems with sending an EXISTS after deleting - // messages, mostly after moving message to other folder. It causes - // Outlook to rebuild the whole mailbox. [RFC-3501] says it's not - // necessary to send an EXISTS response with the new value. - } - return nil -} - -func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error { - total, unread, unreadSeqNum, err := storeMailbox.txGetCounts(tx) - if err != nil { - return errors.Wrap(err, "cannot get counts for mailbox status update") - } - storeMailbox.store.notifyMailboxStatus( - storeMailbox.storeAddress.address, - storeMailbox.labelName, - total, - unread, - unreadSeqNum, - ) - return nil -} - -func (storeMailbox *Mailbox) txMarkMessagesAsDeleted(tx *bolt.Tx, apiIDs []string, markAsDeleted bool) error { - // Load all buckets before looping over apiIDs - metaBucket := tx.Bucket(metadataBucket) - apiBucket := storeMailbox.txGetAPIIDsBucket(tx) - uidBucket := storeMailbox.txGetIMAPIDsBucket(tx) - deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx) - for _, apiID := range apiIDs { - if markAsDeleted { - if err := deletedBucket.Put([]byte(apiID), []byte{1}); err != nil { - return err - } - } else { - if err := deletedBucket.Delete([]byte(apiID)); err != nil { - return err - } - } - - msg, err := storeMailbox.store.txGetMessageFromBucket(metaBucket, apiID) - if err != nil { - return err - } - - uid, err := storeMailbox.txGetUIDFromBucket(apiBucket, apiID) - if err != nil { - return err - } - - seqNum, err := storeMailbox.txGetSequenceNumberOfUID(uidBucket, itob(uid)) - if err != nil { - return err - } - - // In order to send flags in format - // S: * 2 FETCH (FLAGS (\Deleted \Seen)) - storeMailbox.store.notifyUpdateMessage( - storeMailbox.storeAddress.address, - storeMailbox.labelName, - uid, - seqNum, - msg, - markAsDeleted, - ) - } - - return nil -} diff --git a/internal/store/main_test.go b/internal/store/main_test.go deleted file mode 100644 index e3acd34e..00000000 --- a/internal/store/main_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "os" - - "github.com/sirupsen/logrus" -) - -func init() { //nolint:gochecknoinits - logrus.SetLevel(logrus.ErrorLevel) - switch os.Getenv("VERBOSITY") { - case "trace": - logrus.SetLevel(logrus.TraceLevel) - case "debug": - logrus.SetLevel(logrus.DebugLevel) - } -} diff --git a/internal/store/message.go b/internal/store/message.go deleted file mode 100644 index ac8c7ec0..00000000 --- a/internal/store/message.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "bufio" - "bytes" - "net/textproto" - - pkgMsg "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - bolt "go.etcd.io/bbolt" -) - -// Message is wrapper around `pmapi.Message` with connection to -// a specific mailbox with helper functions to get IMAP UID, sequence -// numbers and similar. -type Message struct { - msg *pmapi.Message - - store *Store - storeMailbox *Mailbox -} - -func newStoreMessage(storeMailbox *Mailbox, msg *pmapi.Message) *Message { - return &Message{ - msg: msg, - store: storeMailbox.store, - storeMailbox: storeMailbox, - } -} - -// ID returns message ID on our API (always the same ID for all mailboxes). -func (message *Message) ID() string { - return message.msg.ID -} - -// UID returns message UID for IMAP, specific for mailbox used to get the message. -func (message *Message) UID() (uint32, error) { - return message.storeMailbox.getUID(message.ID()) -} - -// SequenceNumber returns index of message in used mailbox. -func (message *Message) SequenceNumber() (uint32, error) { - return message.storeMailbox.getSequenceNumber(message.ID()) -} - -// Message returns message struct from pmapi. -func (message *Message) Message() *pmapi.Message { - return message.msg -} - -// IsMarkedDeleted returns true if message is marked as deleted for specific mailbox. -func (message *Message) IsMarkedDeleted() bool { - var isMarkedAsDeleted bool - - if err := message.storeMailbox.db().View(func(tx *bolt.Tx) error { - isMarkedAsDeleted = message.storeMailbox.txGetDeletedIDsBucket(tx).Get([]byte(message.msg.ID)) != nil - return nil - }); err != nil { - message.storeMailbox.log.WithError(err).Error("Not able to retrieve deleted mark, assuming false.") - return false - } - - return isMarkedAsDeleted -} - -// IsFullHeaderCached will check that valid full header is stored in DB. -func (message *Message) IsFullHeaderCached() bool { - var raw []byte - err := message.store.db.View(func(tx *bolt.Tx) error { - raw = tx.Bucket(bodystructureBucket).Get([]byte(message.ID())) - return nil - }) - return err == nil && raw != nil -} - -func (message *Message) getRawHeader() ([]byte, error) { - bs, err := message.GetBodyStructure() - if err != nil { - return nil, err - } - - return bs.GetMailHeaderBytes() -} - -// GetHeader will return cached header from DB. -func (message *Message) GetHeader() ([]byte, error) { - raw, err := message.getRawHeader() - if err != nil { - message.store.log. - WithField("msgID", message.ID()). - WithError(err). - Warn("Cannot get raw header") - return nil, err - } - - return raw, nil -} - -// GetMIMEHeaderFast returns full header if message was cached. If full header -// is not available it will return header from metadata. -// NOTE: Returned header may not contain all fields. -func (message *Message) GetMIMEHeaderFast() (header textproto.MIMEHeader) { - var err error - if message.IsFullHeaderCached() { - header, err = message.GetMIMEHeader() - } - if header == nil || err != nil { - header = textproto.MIMEHeader(message.Message().Header) - } - return -} - -// GetMIMEHeader will return cached header from DB, parsed as a textproto.MIMEHeader. -func (message *Message) GetMIMEHeader() (textproto.MIMEHeader, error) { - raw, err := message.getRawHeader() - if err != nil { - message.store.log. - WithField("msgID", message.ID()). - WithError(err). - Warn("Cannot get raw header for MIME header") - return nil, err - } - - header, err := textproto.NewReader(bufio.NewReader(bytes.NewReader(raw))).ReadMIMEHeader() - if err != nil { - message.store.log. - WithField("msgID", message.ID()). - WithError(err). - Warn("Cannot build header from bodystructure") - return nil, err - } - - return header, nil -} - -// GetBodyStructure returns the message's body structure. -// It checks first if it's in the store. If it is, it returns it from store, -// otherwise it computes it from the message cache (and saves the result to the store). -func (message *Message) GetBodyStructure() (*pkgMsg.BodyStructure, error) { - var raw []byte - - if err := message.store.db.View(func(tx *bolt.Tx) error { - raw = tx.Bucket(bodystructureBucket).Get([]byte(message.ID())) - return nil - }); err != nil { - return nil, err - } - - if len(raw) > 0 { - // If not possible to deserialize just continue with build. - if bs, err := pkgMsg.DeserializeBodyStructure(raw); err == nil { - return bs, nil - } - } - - literal, err := message.store.getCachedMessage(message.ID()) - if err != nil { - return nil, err - } - - bs, err := pkgMsg.NewBodyStructure(bytes.NewReader(literal)) - if err != nil { - return nil, err - } - - // Do not cache draft bodystructure - if message.msg.IsDraft() { - return bs, nil - } - - if raw, err = bs.Serialize(); err != nil { - return nil, err - } - - if err := message.store.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(bodystructureBucket).Put([]byte(message.ID()), raw) - }); err != nil { - return nil, err - } - - return bs, nil -} - -// GetRFC822 returns the raw message literal. -func (message *Message) GetRFC822() ([]byte, error) { - return message.store.getCachedMessage(message.ID()) -} - -// GetRFC822Size returns the size of the raw message literal. -func (message *Message) GetRFC822Size() (uint32, error) { - var raw []byte - - if err := message.store.db.View(func(tx *bolt.Tx) error { - raw = tx.Bucket(sizeBucket).Get([]byte(message.ID())) - return nil - }); err != nil { - return 0, err - } - - if len(raw) > 0 { - return btoi(raw), nil - } - - literal, err := message.store.getCachedMessage(message.ID()) - if err != nil { - return 0, err - } - - // Do not cache draft size - if !message.msg.IsDraft() { - if err := message.store.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(sizeBucket).Put([]byte(message.ID()), itob(uint32(len(literal)))) - }); err != nil { - return 0, err - } - } - - return uint32(len(literal)), nil -} diff --git a/internal/store/mocks/mocks.go b/internal/store/mocks/mocks.go deleted file mode 100644 index 39cef924..00000000 --- a/internal/store/mocks/mocks.go +++ /dev/null @@ -1,383 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ProtonMail/proton-bridge/v2/internal/store (interfaces: PanicHandler,BridgeUser,ChangeNotifier,Storer) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - context "context" - reflect "reflect" - - pmapi "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - gomock "github.com/golang/mock/gomock" -) - -// MockPanicHandler is a mock of PanicHandler interface. -type MockPanicHandler struct { - ctrl *gomock.Controller - recorder *MockPanicHandlerMockRecorder -} - -// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler. -type MockPanicHandlerMockRecorder struct { - mock *MockPanicHandler -} - -// NewMockPanicHandler creates a new mock instance. -func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler { - mock := &MockPanicHandler{ctrl: ctrl} - mock.recorder = &MockPanicHandlerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder { - return m.recorder -} - -// HandlePanic mocks base method. -func (m *MockPanicHandler) HandlePanic() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "HandlePanic") -} - -// HandlePanic indicates an expected call of HandlePanic. -func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic)) -} - -// MockBridgeUser is a mock of BridgeUser interface. -type MockBridgeUser struct { - ctrl *gomock.Controller - recorder *MockBridgeUserMockRecorder -} - -// MockBridgeUserMockRecorder is the mock recorder for MockBridgeUser. -type MockBridgeUserMockRecorder struct { - mock *MockBridgeUser -} - -// NewMockBridgeUser creates a new mock instance. -func NewMockBridgeUser(ctrl *gomock.Controller) *MockBridgeUser { - mock := &MockBridgeUser{ctrl: ctrl} - mock.recorder = &MockBridgeUserMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockBridgeUser) EXPECT() *MockBridgeUserMockRecorder { - return m.recorder -} - -// CloseAllConnections mocks base method. -func (m *MockBridgeUser) CloseAllConnections() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "CloseAllConnections") -} - -// CloseAllConnections indicates an expected call of CloseAllConnections. -func (mr *MockBridgeUserMockRecorder) CloseAllConnections() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseAllConnections", reflect.TypeOf((*MockBridgeUser)(nil).CloseAllConnections)) -} - -// CloseConnection mocks base method. -func (m *MockBridgeUser) CloseConnection(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "CloseConnection", arg0) -} - -// CloseConnection indicates an expected call of CloseConnection. -func (mr *MockBridgeUserMockRecorder) CloseConnection(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseConnection", reflect.TypeOf((*MockBridgeUser)(nil).CloseConnection), arg0) -} - -// GetAddressID mocks base method. -func (m *MockBridgeUser) GetAddressID(arg0 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAddressID", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAddressID indicates an expected call of GetAddressID. -func (mr *MockBridgeUserMockRecorder) GetAddressID(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAddressID", reflect.TypeOf((*MockBridgeUser)(nil).GetAddressID), arg0) -} - -// GetClient mocks base method. -func (m *MockBridgeUser) GetClient() pmapi.Client { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetClient") - ret0, _ := ret[0].(pmapi.Client) - return ret0 -} - -// GetClient indicates an expected call of GetClient. -func (mr *MockBridgeUserMockRecorder) GetClient() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockBridgeUser)(nil).GetClient)) -} - -// GetPrimaryAddress mocks base method. -func (m *MockBridgeUser) GetPrimaryAddress() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPrimaryAddress") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetPrimaryAddress indicates an expected call of GetPrimaryAddress. -func (mr *MockBridgeUserMockRecorder) GetPrimaryAddress() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrimaryAddress", reflect.TypeOf((*MockBridgeUser)(nil).GetPrimaryAddress)) -} - -// GetStoreAddresses mocks base method. -func (m *MockBridgeUser) GetStoreAddresses() []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetStoreAddresses") - ret0, _ := ret[0].([]string) - return ret0 -} - -// GetStoreAddresses indicates an expected call of GetStoreAddresses. -func (mr *MockBridgeUserMockRecorder) GetStoreAddresses() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStoreAddresses", reflect.TypeOf((*MockBridgeUser)(nil).GetStoreAddresses)) -} - -// ID mocks base method. -func (m *MockBridgeUser) ID() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ID") - ret0, _ := ret[0].(string) - return ret0 -} - -// ID indicates an expected call of ID. -func (mr *MockBridgeUserMockRecorder) ID() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockBridgeUser)(nil).ID)) -} - -// IsCombinedAddressMode mocks base method. -func (m *MockBridgeUser) IsCombinedAddressMode() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsCombinedAddressMode") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsCombinedAddressMode indicates an expected call of IsCombinedAddressMode. -func (mr *MockBridgeUserMockRecorder) IsCombinedAddressMode() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCombinedAddressMode", reflect.TypeOf((*MockBridgeUser)(nil).IsCombinedAddressMode)) -} - -// IsConnected mocks base method. -func (m *MockBridgeUser) IsConnected() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsConnected") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsConnected indicates an expected call of IsConnected. -func (mr *MockBridgeUserMockRecorder) IsConnected() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockBridgeUser)(nil).IsConnected)) -} - -// Logout mocks base method. -func (m *MockBridgeUser) Logout() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Logout") - ret0, _ := ret[0].(error) - return ret0 -} - -// Logout indicates an expected call of Logout. -func (mr *MockBridgeUserMockRecorder) Logout() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockBridgeUser)(nil).Logout)) -} - -// UpdateSpace mocks base method. -func (m *MockBridgeUser) UpdateSpace(arg0 *pmapi.User) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "UpdateSpace", arg0) -} - -// UpdateSpace indicates an expected call of UpdateSpace. -func (mr *MockBridgeUserMockRecorder) UpdateSpace(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSpace", reflect.TypeOf((*MockBridgeUser)(nil).UpdateSpace), arg0) -} - -// UpdateUser mocks base method. -func (m *MockBridgeUser) UpdateUser(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUser", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateUser indicates an expected call of UpdateUser. -func (mr *MockBridgeUserMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockBridgeUser)(nil).UpdateUser), arg0) -} - -// MockChangeNotifier is a mock of ChangeNotifier interface. -type MockChangeNotifier struct { - ctrl *gomock.Controller - recorder *MockChangeNotifierMockRecorder -} - -// MockChangeNotifierMockRecorder is the mock recorder for MockChangeNotifier. -type MockChangeNotifierMockRecorder struct { - mock *MockChangeNotifier -} - -// NewMockChangeNotifier creates a new mock instance. -func NewMockChangeNotifier(ctrl *gomock.Controller) *MockChangeNotifier { - mock := &MockChangeNotifier{ctrl: ctrl} - mock.recorder = &MockChangeNotifierMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockChangeNotifier) EXPECT() *MockChangeNotifierMockRecorder { - return m.recorder -} - -// CanDelete mocks base method. -func (m *MockChangeNotifier) CanDelete(arg0 string) (bool, func()) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CanDelete", arg0) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(func()) - return ret0, ret1 -} - -// CanDelete indicates an expected call of CanDelete. -func (mr *MockChangeNotifierMockRecorder) CanDelete(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanDelete", reflect.TypeOf((*MockChangeNotifier)(nil).CanDelete), arg0) -} - -// DeleteMessage mocks base method. -func (m *MockChangeNotifier) DeleteMessage(arg0, arg1 string, arg2 uint32) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteMessage", arg0, arg1, arg2) -} - -// DeleteMessage indicates an expected call of DeleteMessage. -func (mr *MockChangeNotifierMockRecorder) DeleteMessage(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockChangeNotifier)(nil).DeleteMessage), arg0, arg1, arg2) -} - -// MailboxCreated mocks base method. -func (m *MockChangeNotifier) MailboxCreated(arg0, arg1 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "MailboxCreated", arg0, arg1) -} - -// MailboxCreated indicates an expected call of MailboxCreated. -func (mr *MockChangeNotifierMockRecorder) MailboxCreated(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxCreated", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxCreated), arg0, arg1) -} - -// MailboxStatus mocks base method. -func (m *MockChangeNotifier) MailboxStatus(arg0, arg1 string, arg2, arg3, arg4 uint32) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "MailboxStatus", arg0, arg1, arg2, arg3, arg4) -} - -// MailboxStatus indicates an expected call of MailboxStatus. -func (mr *MockChangeNotifierMockRecorder) MailboxStatus(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxStatus", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxStatus), arg0, arg1, arg2, arg3, arg4) -} - -// Notice mocks base method. -func (m *MockChangeNotifier) Notice(arg0, arg1 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Notice", arg0, arg1) -} - -// Notice indicates an expected call of Notice. -func (mr *MockChangeNotifierMockRecorder) Notice(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notice", reflect.TypeOf((*MockChangeNotifier)(nil).Notice), arg0, arg1) -} - -// UpdateMessage mocks base method. -func (m *MockChangeNotifier) UpdateMessage(arg0, arg1 string, arg2, arg3 uint32, arg4 *pmapi.Message, arg5 bool) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "UpdateMessage", arg0, arg1, arg2, arg3, arg4, arg5) -} - -// UpdateMessage indicates an expected call of UpdateMessage. -func (mr *MockChangeNotifierMockRecorder) UpdateMessage(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMessage", reflect.TypeOf((*MockChangeNotifier)(nil).UpdateMessage), arg0, arg1, arg2, arg3, arg4, arg5) -} - -// MockStorer is a mock of Storer interface. -type MockStorer struct { - ctrl *gomock.Controller - recorder *MockStorerMockRecorder -} - -// MockStorerMockRecorder is the mock recorder for MockStorer. -type MockStorerMockRecorder struct { - mock *MockStorer -} - -// NewMockStorer creates a new mock instance. -func NewMockStorer(ctrl *gomock.Controller) *MockStorer { - mock := &MockStorer{ctrl: ctrl} - mock.recorder = &MockStorerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStorer) EXPECT() *MockStorerMockRecorder { - return m.recorder -} - -// BuildAndCacheMessage mocks base method. -func (m *MockStorer) BuildAndCacheMessage(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BuildAndCacheMessage", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// BuildAndCacheMessage indicates an expected call of BuildAndCacheMessage. -func (mr *MockStorerMockRecorder) BuildAndCacheMessage(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildAndCacheMessage", reflect.TypeOf((*MockStorer)(nil).BuildAndCacheMessage), arg0, arg1) -} - -// IsCached mocks base method. -func (m *MockStorer) IsCached(arg0 string) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsCached", arg0) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsCached indicates an expected call of IsCached. -func (mr *MockStorerMockRecorder) IsCached(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCached", reflect.TypeOf((*MockStorer)(nil).IsCached), arg0) -} diff --git a/internal/store/mocks/utils_mocks.go b/internal/store/mocks/utils_mocks.go deleted file mode 100644 index e60e9f03..00000000 --- a/internal/store/mocks/utils_mocks.go +++ /dev/null @@ -1,133 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ProtonMail/proton-bridge/v2/pkg/listener (interfaces: Listener) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - time "time" - - gomock "github.com/golang/mock/gomock" -) - -// MockListener is a mock of Listener interface. -type MockListener struct { - ctrl *gomock.Controller - recorder *MockListenerMockRecorder -} - -// MockListenerMockRecorder is the mock recorder for MockListener. -type MockListenerMockRecorder struct { - mock *MockListener -} - -// NewMockListener creates a new mock instance. -func NewMockListener(ctrl *gomock.Controller) *MockListener { - mock := &MockListener{ctrl: ctrl} - mock.recorder = &MockListenerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockListener) EXPECT() *MockListenerMockRecorder { - return m.recorder -} - -// Add mocks base method. -func (m *MockListener) Add(arg0 string, arg1 chan<- string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Add", arg0, arg1) -} - -// Add indicates an expected call of Add. -func (mr *MockListenerMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockListener)(nil).Add), arg0, arg1) -} - -// Book mocks base method. -func (m *MockListener) Book(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Book", arg0) -} - -// Book indicates an expected call of Book. -func (mr *MockListenerMockRecorder) Book(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Book", reflect.TypeOf((*MockListener)(nil).Book), arg0) -} - -// Emit mocks base method. -func (m *MockListener) Emit(arg0, arg1 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Emit", arg0, arg1) -} - -// Emit indicates an expected call of Emit. -func (mr *MockListenerMockRecorder) Emit(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), arg0, arg1) -} - -// ProvideChannel mocks base method. -func (m *MockListener) ProvideChannel(arg0 string) <-chan string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProvideChannel", arg0) - ret0, _ := ret[0].(<-chan string) - return ret0 -} - -// ProvideChannel indicates an expected call of ProvideChannel. -func (mr *MockListenerMockRecorder) ProvideChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProvideChannel", reflect.TypeOf((*MockListener)(nil).ProvideChannel), arg0) -} - -// Remove mocks base method. -func (m *MockListener) Remove(arg0 string, arg1 chan<- string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Remove", arg0, arg1) -} - -// Remove indicates an expected call of Remove. -func (mr *MockListenerMockRecorder) Remove(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockListener)(nil).Remove), arg0, arg1) -} - -// RetryEmit mocks base method. -func (m *MockListener) RetryEmit(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "RetryEmit", arg0) -} - -// RetryEmit indicates an expected call of RetryEmit. -func (mr *MockListenerMockRecorder) RetryEmit(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryEmit", reflect.TypeOf((*MockListener)(nil).RetryEmit), arg0) -} - -// SetBuffer mocks base method. -func (m *MockListener) SetBuffer(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetBuffer", arg0) -} - -// SetBuffer indicates an expected call of SetBuffer. -func (mr *MockListenerMockRecorder) SetBuffer(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBuffer", reflect.TypeOf((*MockListener)(nil).SetBuffer), arg0) -} - -// SetLimit mocks base method. -func (m *MockListener) SetLimit(arg0 string, arg1 time.Duration) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetLimit", arg0, arg1) -} - -// SetLimit indicates an expected call of SetLimit. -func (mr *MockListenerMockRecorder) SetLimit(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockListener)(nil).SetLimit), arg0, arg1) -} diff --git a/internal/store/store.go b/internal/store/store.go deleted file mode 100644 index d8a7eb68..00000000 --- a/internal/store/store.go +++ /dev/null @@ -1,483 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package store communicates with API and caches metadata in a local database. -package store - -import ( - "context" - "fmt" - "os" - "sync" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/sentry" - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/ProtonMail/proton-bridge/v2/pkg/pool" - "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - bolt "go.etcd.io/bbolt" -) - -const ( - // PathDelimiter for IMAP. - PathDelimiter = "/" - // UserLabelsMailboxName for IMAP. - UserLabelsMailboxName = "Labels" - // UserLabelsPrefix contains name with delimiter for IMAP. - UserLabelsPrefix = UserLabelsMailboxName + PathDelimiter - // UserFoldersMailboxName for IMAP. - UserFoldersMailboxName = "Folders" - // UserFoldersPrefix contains name with delimiter for IMAP. - UserFoldersPrefix = UserFoldersMailboxName + PathDelimiter -) - -var ( - log = logrus.WithField("pkg", "store") //nolint:gochecknoglobals - - // Database structure: - // * metadata - // * {messageID} -> message data (subject, from, to, time, ...) - // * headers - // * {messageID} -> header bytes - // * bodystructure - // * {messageID} -> message body structure - // * size - // * {messageID} -> uint32 value - // * counts - // * {mailboxID} -> mailboxCounts: totalOnAPI, unreadOnAPI, labelName, labelColor, labelIsExclusive - // * address_info - // * {index} -> {address, addressID} - // * address_mode - // * mode -> string split or combined - // * cache_passphrase - // * passphrase -> cache passphrase (pgp encrypted message) - // * mailboxes_version - // * version -> uint32 value - // * sync_state - // * sync_state -> string timestamp when it was last synced (when missing, sync should be ongoing) - // * ids_ranges -> json array of groups with start and end message ID (when missing, there is no ongoing sync) - // * ids_to_be_deleted -> json array of message IDs to be deleted after sync (when missing, there is no ongoing sync) - // * mailboxes - // * {addressID+mailboxID} - // * imap_ids - // * {imapUID} -> string messageID - // * api_ids - // * {messageID} -> uint32 imapUID - // * deleted_ids (can be missing or have no keys) - // * {messageID} -> true - metadataBucket = []byte("metadata") //nolint:gochecknoglobals - headersBucket = []byte("headers") //nolint:gochecknoglobals - bodystructureBucket = []byte("bodystructure") //nolint:gochecknoglobals - sizeBucket = []byte("size") //nolint:gochecknoglobals - countsBucket = []byte("counts") //nolint:gochecknoglobals - addressInfoBucket = []byte("address_info") //nolint:gochecknoglobals - addressModeBucket = []byte("address_mode") //nolint:gochecknoglobals - cachePassphraseBucket = []byte("cache_passphrase") //nolint:gochecknoglobals - syncStateBucket = []byte("sync_state") //nolint:gochecknoglobals - mailboxesBucket = []byte("mailboxes") //nolint:gochecknoglobals - imapIDsBucket = []byte("imap_ids") //nolint:gochecknoglobals - apiIDsBucket = []byte("api_ids") //nolint:gochecknoglobals - deletedIDsBucket = []byte("deleted_ids") //nolint:gochecknoglobals - mboxVersionBucket = []byte("mailboxes_version") //nolint:gochecknoglobals - - // ErrNoSuchAPIID when mailbox does not have API ID. - ErrNoSuchAPIID = errors.New("no such api id") //nolint:gochecknoglobals - // ErrNoSuchUID when mailbox does not have IMAP UID. - ErrNoSuchUID = errors.New("no such uid") //nolint:gochecknoglobals - // ErrNoSuchSeqNum when mailbox does not have IMAP ID. - ErrNoSuchSeqNum = errors.New("no such sequence number") //nolint:gochecknoglobals -) - -// exposeContextForIMAP should be replaced once with context passed -// as an argument from IMAP package and IMAP library should cancel -// context when IMAP client cancels the request. -func exposeContextForIMAP() context.Context { - return context.TODO() -} - -// exposeContextForSMTP is the same as above but for SMTP. -func exposeContextForSMTP() context.Context { - return context.TODO() -} - -// Store is local user storage, which handles the synchronization between IMAP and PM API. -type Store struct { - sentryReporter *sentry.Reporter - panicHandler PanicHandler - user BridgeUser - eventLoop *eventLoop - currentEvents *Events - - log *logrus.Entry - - filePath string - db *bolt.DB - lock *sync.RWMutex - addresses map[string]*Address - notifier ChangeNotifier - - builder *message.Builder - cache cache.Cache - msgCachePool *MsgCachePool - done chan struct{} - - isSyncRunning bool - syncCooldown cooldown - addressMode addressMode -} - -// New creates or opens a store for the given `user`. -func New( //nolint:funlen - sentryReporter *sentry.Reporter, - panicHandler PanicHandler, - user BridgeUser, - listener listener.Listener, - cache cache.Cache, - builder *message.Builder, - path string, - currentEvents *Events, -) (store *Store, err error) { - if user == nil || listener == nil || currentEvents == nil { - return nil, fmt.Errorf("missing parameters - user: %v, listener: %v, currentEvents: %v", user, listener, currentEvents) - } - - l := log.WithField("user", user.ID()) - - var firstInit bool - if _, existErr := os.Stat(path); os.IsNotExist(existErr) { - l.Info("Creating new store database file with address mode from user's credentials store") - firstInit = true - } else { - l.Info("Store database file already exists, using mode already set") - firstInit = false - } - - bdb, err := openBoltDatabase(path) - if err != nil { - return nil, errors.Wrap(err, "failed to open store database") - } - - store = &Store{ - sentryReporter: sentryReporter, - panicHandler: panicHandler, - user: user, - currentEvents: currentEvents, - - log: l, - - filePath: path, - db: bdb, - lock: &sync.RWMutex{}, - - builder: builder, - cache: cache, - } - - // Create a new cacher. It's not started yet. - // NOTE(GODT-1158): I hate this circular dependency store->cacher->store :( - store.msgCachePool = newMsgCachePool(store) - - // Minimal increase is event pollInterval, doubles every failed retry up to 5 minutes. - store.syncCooldown.setExponentialWait(pollInterval, 2, 5*time.Minute) - - if err = store.init(firstInit); err != nil { - l.WithError(err).Error("Could not initialise store, attempting to close") - if storeCloseErr := store.Close(); storeCloseErr != nil { - l.WithError(storeCloseErr).Warn("Could not close uninitialised store") - } - err = errors.Wrap(err, "failed to initialise store") - return - } - - if user.IsConnected() { - store.eventLoop = newEventLoop(currentEvents, store, user, listener) - go func() { - defer store.panicHandler.HandlePanic() - store.eventLoop.start() - }() - } - - return store, err -} - -func openBoltDatabase(filePath string) (db *bolt.DB, err error) { - l := log.WithField("path", filePath) - l.Debug("Opening bolt database") - - if db, err = bolt.Open(filePath, 0o600, &bolt.Options{Timeout: 1 * time.Second}); err != nil { - l.WithError(err).Error("Could not open bolt database") - return - } - - if val, set := os.LookupEnv("BRIDGESTRICTMODE"); set && val == "1" { - db.StrictMode = true - } - - tx := func(tx *bolt.Tx) (err error) { - buckets := [][]byte{ - metadataBucket, - headersBucket, - bodystructureBucket, - sizeBucket, - countsBucket, - addressInfoBucket, - addressModeBucket, - cachePassphraseBucket, - syncStateBucket, - mailboxesBucket, - mboxVersionBucket, - } - - for _, bucket := range buckets { - if _, err = tx.CreateBucketIfNotExists(bucket); err != nil { - err = errors.Wrap(err, string(bucket)) - return - } - } - - return - } - - if err = db.Update(tx); err != nil { - return - } - - return db, err -} - -// init initialises the store for the given addresses. -func (store *Store) init(firstInit bool) (err error) { - if store.addresses != nil { - store.log.Warn("Store was already initialised") - return - } - - // If it's the first time we are creating the store, use the mode set in the - // user's credentials, otherwise read it from the DB (if present). - if firstInit { - if store.user.IsCombinedAddressMode() { - err = store.setAddressMode(combinedMode) - } else { - err = store.setAddressMode(splitMode) - } - if err != nil { - return errors.Wrap(err, "first init setting store address mode") - } - } else if store.addressMode, err = store.getAddressMode(); err != nil { - store.log.WithError(err).Error("Store address mode is unknown, setting to combined mode") - if err = store.setAddressMode(combinedMode); err != nil { - return errors.Wrap(err, "setting store address mode") - } - } - - store.log.WithField("mode", store.addressMode).Info("Initialising store") - - labels, err := store.initCounts() - if err != nil { - store.log.WithError(err).Error("Could not initialise label counts") - return - } - - if err = store.initAddresses(labels); err != nil { - store.log.WithError(err).Error("Could not initialise store addresses") - return - } - - return err -} - -func (store *Store) client() pmapi.Client { - return store.user.GetClient() -} - -// initCounts initialises the counts for each label. It tries to use the API first to fetch the labels but if -// the API is unavailable for whatever reason it tries to fetch the labels locally. -func (store *Store) initCounts() (labels []*pmapi.Label, err error) { - if labels, err = store.client().ListLabels(pmapi.ContextWithoutRetry(context.Background())); err != nil { - store.log.WithError(err).Warn("Could not list API labels. Trying with local labels.") - if labels, err = store.getLabelsFromLocalStorage(); err != nil { - store.log.WithError(err).Error("Cannot list local labels") - return - } - } else { - // the labels listed by PMAPI don't include system folders so we need to add them. - for _, counts := range getSystemFolders() { - labels = append(labels, counts.getPMLabel()) - } - - if err = store.createOrUpdateMailboxCountsBuckets(labels); err != nil { - store.log.WithError(err).Error("Cannot create counts") - return - } - - if countsErr := store.updateCountsFromServer(); countsErr != nil { - store.log.WithError(countsErr).Warning("Continue without new counts from server") - } - } - - sortByOrder(labels) - - return -} - -// initAddresses creates address objects in the store for each necessary address. -// In combined mode this means just one mailbox for all addresses but in split mode this means one mailbox per address. -func (store *Store) initAddresses(labels []*pmapi.Label) (err error) { - store.addresses = make(map[string]*Address) - - addrInfo, err := store.GetAddressInfo() - if err != nil { - store.log.WithError(err).Error("Could not get addresses and address IDs") - return - } - - // We need at least one address to continue. - if len(addrInfo) < 1 { - err = errors.New("no addresses to initialise") - store.log.WithError(err).Warn("There are no addresses to initialise") - return - } - - // If in combined mode, we only need the user's primary address. - if store.addressMode == combinedMode { - addrInfo = addrInfo[:1] - } - - for _, addr := range addrInfo { - if err = store.addAddress(addr.Address, addr.AddressID, labels); err != nil { - store.log.WithField("address", addr.Address).WithError(err).Error("Could not add address to store") - } - } - - return err -} - -// addAddress adds a new address to the store. If the address exists already it is overwritten. -func (store *Store) addAddress(address, addressID string, labels []*pmapi.Label) (err error) { - if _, ok := store.addresses[addressID]; ok { - store.log.WithField("addressID", addressID).Debug("Overwriting store address") - } - - addr, err := newAddress(store, address, addressID, labels) - if err != nil { - return errors.Wrap(err, "failed to create store address object") - } - - store.addresses[addressID] = addr - - return -} - -// newBuildJob returns a new build job for the given message using the store's message builder. -func (store *Store) newBuildJob(ctx context.Context, messageID string, priority int) (*message.Job, pool.DoneFunc) { - return store.builder.NewJobWithOptions( - ctx, - store.client(), - messageID, - 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. - }, - priority, - ) -} - -// Close stops the event loop and closes the database to free the file. -func (store *Store) Close() error { - store.lock.Lock() - defer store.lock.Unlock() - - return store.close() -} - -// CloseEventLoopAndCacher stops the eventloop (if it is present). -func (store *Store) CloseEventLoopAndCacher() { - if store.eventLoop != nil { - store.eventLoop.stop() - } - - store.stopWatcher() - - store.msgCachePool.stop() -} - -func (store *Store) close() error { - // Stop the event loop and cacher first before closing the DB. - store.CloseEventLoopAndCacher() - - // Close the database. - return store.db.Close() -} - -// Remove closes and removes the database file and clears the cache file. -func (store *Store) Remove() error { - store.lock.Lock() - defer store.lock.Unlock() - - store.log.Trace("Removing store") - - var result *multierror.Error - - if err := store.close(); err != nil { - result = multierror.Append(result, errors.Wrap(err, "failed to close store")) - } - - if err := RemoveStore(store.currentEvents, store.filePath, store.user.ID()); err != nil { - result = multierror.Append(result, errors.Wrap(err, "failed to remove store")) - } - - if err := store.RemoveCache(); err != nil { - result = multierror.Append(result, errors.Wrap(err, "failed to remove cache")) - } - - return result.ErrorOrNil() -} - -func (store *Store) RemoveCache() error { - store.stopWatcher() - - if err := store.clearCachePassphrase(); err != nil { - logrus.WithError(err).Error("Failed to clear cache passphrase") - } - - return store.cache.Delete(store.user.ID()) -} - -// RemoveStore removes the database file and clears the cache file. -func RemoveStore(currentEvents *Events, path, userID string) error { - var result *multierror.Error - - if err := currentEvents.clearUserEvents(userID); err != nil { - result = multierror.Append(result, errors.Wrap(err, "failed to clear event loop user cache")) - } - - // RemoveAll will not return an error if the path does not exist. - if err := os.RemoveAll(path); err != nil { - result = multierror.Append(result, errors.Wrap(err, "failed to remove database file")) - } - - return result.ErrorOrNil() -} diff --git a/internal/store/store_address_mode.go b/internal/store/store_address_mode.go deleted file mode 100644 index 2d7a0b0f..00000000 --- a/internal/store/store_address_mode.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "github.com/pkg/errors" - bolt "go.etcd.io/bbolt" -) - -type addressMode string - -const ( - splitMode addressMode = "split" - combinedMode addressMode = "combined" - modeKey = "mode" -) - -// getAddressMode returns the current address mode (split or combined) of the store. -// It first looks in the local cache but if that is not yet set, it loads it from the database. -func (store *Store) getAddressMode() (mode addressMode, err error) { - if store.addressMode != "" { - mode = store.addressMode - return - } - - tx := func(tx *bolt.Tx) (err error) { - b := tx.Bucket(addressModeBucket) - - dbMode := b.Get([]byte(modeKey)) - if dbMode == nil { - return errors.New("address mode not set") - } - - mode = addressMode(dbMode) - - return - } - - err = store.db.View(tx) - - return -} - -// IsCombinedMode returns whether the store is set to combined mode. -func (store *Store) IsCombinedMode() bool { - return store.addressMode == combinedMode -} - -// UseCombinedMode sets whether the store should be set to combined mode. -func (store *Store) UseCombinedMode(useCombined bool) (err error) { - if useCombined { - err = store.switchAddressMode(combinedMode) - } else { - err = store.switchAddressMode(splitMode) - } - - return -} - -// switchAddressMode sets the address mode to the given value and rebuilds the mailboxes. -func (store *Store) switchAddressMode(mode addressMode) (err error) { - if store.addressMode == mode { - log.Debug("The store is using the correct address mode") - return - } - - if err = store.setAddressMode(mode); err != nil { - log.WithError(err).Error("Could not set store address mode") - return - } - - if err = store.RebuildMailboxes(); err != nil { - log.WithError(err).Error("Could not rebuild mailboxes after switching address mode") - return - } - - return -} - -// setAddressMode sets the current address mode (split or combined) of the store. -// It writes to database and updates the local value in the store object. -func (store *Store) setAddressMode(mode addressMode) (err error) { - store.log.WithField("mode", string(mode)).Info("Setting store address mode") - - tx := func(tx *bolt.Tx) (err error) { - b := tx.Bucket(addressModeBucket) - return b.Put([]byte(modeKey), []byte(mode)) - } - - if err = store.db.Update(tx); err != nil { - return - } - - store.addressMode = mode - - return -} diff --git a/internal/store/store_structure_version.go b/internal/store/store_structure_version.go deleted file mode 100644 index 689d5bea..00000000 --- a/internal/store/store_structure_version.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import bolt "go.etcd.io/bbolt" - -const ( - versionKey = "version" - - // versionOffset makes it possible to force email client to reload all - // mailboxes. If increased during application update it will trigger - // the reload on client side without needing to sync DB or re-setup account. - versionOffset = uint32(3) -) - -func (store *Store) getMailboxesVersion() uint32 { - localVersion := store.readMailboxesVersion() - // If a read error occurs it returns 0 which is an invalid version value. - if localVersion == 0 { - localVersion = 1 - _ = store.writeMailboxesVersion(localVersion) - } - - // versionOffset will make email clients reload if increased during bridge update. - return localVersion + versionOffset -} - -func (store *Store) increaseMailboxesVersion() error { - ver := store.readMailboxesVersion() - // The version is zero if a read error occurred. Operation ++ will make it 1 - // which is default starting value. - ver++ - return store.writeMailboxesVersion(ver) -} - -func (store *Store) readMailboxesVersion() (version uint32) { - _ = store.db.View(func(tx *bolt.Tx) (err error) { - b := tx.Bucket(mboxVersionBucket) - verRaw := b.Get([]byte(versionKey)) - if verRaw != nil { - version = btoi(verRaw) - } - return nil - }) - return -} - -func (store *Store) writeMailboxesVersion(ver uint32) error { - return store.db.Update(func(tx *bolt.Tx) (err error) { - b := tx.Bucket(mboxVersionBucket) - return b.Put([]byte(versionKey), itob(ver)) - }) -} diff --git a/internal/store/store_test.go b/internal/store/store_test.go deleted file mode 100644 index 5f4446bb..00000000 --- a/internal/store/store_test.go +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - "fmt" - "os" - "path/filepath" - "runtime" - "testing" - "time" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" - storemocks "github.com/ProtonMail/proton-bridge/v2/internal/store/mocks" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - pmapimocks "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi/mocks" - tests "github.com/ProtonMail/proton-bridge/v2/test" - "github.com/golang/mock/gomock" - - "github.com/stretchr/testify/require" -) - -const ( - addr1 = "niceaddress@pm.me" - addrID1 = "niceaddressID" - - addr2 = "jamesandmichalarecool@pm.me" - addrID2 = "jamesandmichalarecool" - - testPrivateKeyPassword = "apple" - testPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -Version: OpenPGP.js v0.7.1 -Comment: http://openpgpjs.org - -xcMGBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE -WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39 -vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi -MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5 -c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb -DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB -AAH+CQMIvzcDReuJkc9gnxAkfgmnkBFwRQrqT/4UAPOF8WGVo0uNvDo7Snlk -qWsJS+54+/Xx6Jur/PdBWeEu+6+6GnppYuvsaT0D0nFdFhF6pjng+02IOxfG -qlYXYcW4hRru3BfvJlSvU2LL/Z/ooBnw3T5vqd0eFHKrvabUuwf0x3+K/sru -Fp24rl2PU+bzQlUgKpWzKDmO+0RdKQ6KVCyCDMIXaAkALwNffAvYxI0wnb2y -WAV/bGn1ODnszOYPk3pEMR6kKSxLLaO69kYx4eTERFyJ+1puAxEPCk3Cfeif -yDWi4rU03YB16XH7hQLSFl61SKeIYlkKmkO5Hk1ybi/BhvOGBPVeGGbxWnwI -46G8DfBHW0+uvD5cAQtk2d/q3Ge1I+DIyvuRCcSu0XSBNv/Bkpp4IbAUPBaW -TIvf5p9oxw+AjrMtTtcdSiee1S6CvMMaHhVD7SI6qGA8GqwaXueeLuEXa0Ok -BWlehx8wibMi4a9fLcQZtzJkmGhR1WzXcJfiEg32srILwIzPQYxuFdZZ2elb -gYp/bMEIp4LKhi43IyM6peCDHDzEba8NuOSd0heEqFIm0vlXujMhkyMUvDBv -H0V5On4aMuw/aSEKcAdbazppOru/W1ndyFa5ZHQIC19g72ZaDVyYjPyvNgOV -AFqO4o3IbC5z31zMlTtMbAq2RG9svwUVejn0tmF6UPluTe0U1NuXFpLK6TCH -wqocLz4ecptfJQulpYjClVLgzaYGDuKwQpIwPWg5G/DtKSCGNtEkfqB3aemH -V5xmoYm1v5CQZAEvvsrLA6jxCk9lzqYV8QMivWNXUG+mneIEM35G0HOPzXca -LLyB+N8Zxioc9DPGfdbcxXuVgOKRepbkq4xv1pUpMQ4BUmlkejDRSP+5SIR3 -iEthg+FU6GRSQbORE6nhrKjGBk8fpNpozQZVc2VySUTCwHIEEAEIACYFAlRJ -bc8GCwkIBwMCCRA+tiWe3yHfJAQVCAIKAxYCAQIbAwIeAQAA9J0H/RLR/Uwt -CakrPKtfeGaNuOI45SRTNxM8TklC6tM28sJSzkX8qKPzvI1PxyLhs/i0/fCQ -7Z5bU6n41oLuqUt2S9vy+ABlChKAeziOqCHUcMzHOtbKiPkKW88aO687nx+A -ol2XOnMTkVIC+edMUgnKp6tKtZnbO4ea6Cg88TFuli4hLHNXTfCECswuxHOc -AO1OKDRrCd08iPI5CLNCIV60QnduitE1vF6ehgrH25Vl6LEdd8vPVlTYAvsa -6ySk2RIrHNLUZZ3iII3MBFL8HyINp/XA1BQP+QbH801uSLq8agxM4iFT9C+O -D147SawUGhjD5RG7T+YtqItzgA1V9l277EXHwwYEVEltzwEIAJD57uX6bOc4 -Tgf3utfL/4hdyoqIMVHkYQOvE27wPsZxX08QsdlaNeGji9Ap2ifIDuckUqn6 -Ji9jtZDKtOzdTBm6rnG5nPmkn6BJXPhnecQRP8N0XBISnAGmE4t+bxtts5Wb -qeMdxJYqMiGqzrLBRJEIDTcg3+QF2Y3RywOqlcXqgG/xX++PsvR1Jiz0rEVP -TcBc7ytyb/Av7mx1S802HRYGJHOFtVLoPTrtPCvv+DRDK8JzxQW2XSQLlI0M -9s1tmYhCogYIIqKx9qOTd5mFJ1hJlL6i9xDkvE21qPFASFtww5tiYmUfFaxI -LwbXPZlQ1I/8fuaUdOxctQ+g40ZgHPcAEQEAAf4JAwgdUg8ubE2BT2DITBD+ -XFgjrnUlQBilbN8/do/36KHuImSPO/GGLzKh4+oXxrvLc5fQLjeO+bzeen4u -COCBRO0hG7KpJPhQ6+T02uEF6LegE1sEz5hp6BpKUdPZ1+8799Rylb5kubC5 -IKnLqqpGDbH3hIsmSV3CG/ESkaGMLc/K0ZPt1JRWtUQ9GesXT0v6fdM5GB/L -cZWFdDoYgZAw5BtymE44knIodfDAYJ4DHnPCh/oilWe1qVTQcNMdtkpBgkuo -THecqEmiODQz5EX8pVmS596XsnPO299Lo3TbaHUQo7EC6Au1Au9+b5hC1pDa -FVCLcproi/Cgch0B/NOCFkVLYmp6BEljRj2dSZRWbO0vgl9kFmJEeiiH41+k -EAI6PASSKZs3BYLFc2I8mBkcvt90kg4MTBjreuk0uWf1hdH2Rv8zprH4h5Uh -gjx5nUDX8WXyeLxTU5EBKry+A2DIe0Gm0/waxp6lBlUl+7ra28KYEoHm8Nq/ -N9FCuEhFkFgw6EwUp7jsrFcqBKvmni6jyplm+mJXi3CK+IiNcqub4XPnBI97 -lR19fupB/Y6M7yEaxIM8fTQXmP+x/fe8zRphdo+7o+pJQ3hk5LrrNPK8GEZ6 -DLDOHjZzROhOgBvWtbxRktHk+f5YpuQL+xWd33IV1xYSSHuoAm0Zwt0QJxBs -oFBwJEq1NWM4FxXJBogvzV7KFhl/hXgtvx+GaMv3y8gucj+gE89xVv0XBXjl -5dy5/PgCI0Id+KAFHyKpJA0N0h8O4xdJoNyIBAwDZ8LHt0vlnLGwcJFR9X7/ -PfWe0PFtC3d7cYY3RopDhnRP7MZs1Wo9nZ4IvlXoEsE2nPkWcns+Wv5Yaewr -s2ra9ZIK7IIJhqKKgmQtCeiXyFwTq+kfunDnxeCavuWL3HuLKIOZf7P9vXXt -XgEir9rCwF8EGAEIABMFAlRJbdIJED62JZ7fId8kAhsMAAD+LAf+KT1EpkwH -0ivTHmYako+6qG6DCtzd3TibWw51cmbY20Ph13NIS/MfBo828S9SXm/sVUzN -/r7qZgZYfI0/j57tG3BguVGm53qya4bINKyi1RjK6aKo/rrzRkh5ZVD5rVNO -E2zzvyYAnLUWG9AV1OYDxcgLrXqEMWlqZAo+Wmg7VrTBmdCGs/BPvscNgQRr -6Gpjgmv9ru6LjRL7vFhEcov/tkBLj+CtaWWFTd1s2vBLOs4rCsD9TT/23vfw -CnokvvVjKYN5oviy61yhpqF1rWlOsxZ4+2sKW3Pq7JLBtmzsZegTONfcQAf7 -qqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA== -=2wIY ------END PGP PRIVATE KEY BLOCK----- -` -) - -var testPrivateKeyRing *crypto.KeyRing - -func init() { - privKey, err := crypto.NewKeyFromArmored(testPrivateKey) - if err != nil { - panic(err) - } - - privKeyUnlocked, err := privKey.Unlock([]byte(testPrivateKeyPassword)) - if err != nil { - panic(err) - } - - if testPrivateKeyRing, err = crypto.NewKeyRing(privKeyUnlocked); err != nil { - panic(err) - } -} - -type mocksForStore struct { - tb testing.TB - - ctrl *gomock.Controller - events *storemocks.MockListener - user *storemocks.MockBridgeUser - client *pmapimocks.MockClient - panicHandler *storemocks.MockPanicHandler - changeNotifier *storemocks.MockChangeNotifier - store *Store - - tmpDir string - cache *Events -} - -func initMocks(tb testing.TB) (*mocksForStore, func()) { - ctrl := gomock.NewController(tb) - mocks := &mocksForStore{ - tb: tb, - ctrl: ctrl, - events: storemocks.NewMockListener(ctrl), - user: storemocks.NewMockBridgeUser(ctrl), - client: pmapimocks.NewMockClient(ctrl), - panicHandler: storemocks.NewMockPanicHandler(ctrl), - changeNotifier: storemocks.NewMockChangeNotifier(ctrl), - } - - // Called during clean-up. - mocks.panicHandler.EXPECT().HandlePanic().AnyTimes() - - var err error - mocks.tmpDir, err = os.MkdirTemp("", "store-test") - require.NoError(tb, err) - - cacheFile := filepath.Join(mocks.tmpDir, "cache.json") - mocks.cache = NewEvents(cacheFile) - - return mocks, func() { - if err := recover(); err != nil { - panic(err) - } - if mocks.store != nil { - require.Nil(tb, mocks.store.Close()) - } - ctrl.Finish() - require.NoError(tb, os.RemoveAll(mocks.tmpDir)) - } -} - -func (mocks *mocksForStore) newStoreNoEvents(t *testing.T, combinedMode bool, msgs ...*pmapi.Message) { //nolint:unparam - mocks.user.EXPECT().ID().Return("userID").AnyTimes() - mocks.user.EXPECT().IsConnected().Return(true) - mocks.user.EXPECT().IsCombinedAddressMode().Return(combinedMode) - - mocks.user.EXPECT().GetClient().AnyTimes().Return(mocks.client) - - testUserKeyring := tests.MakeKeyRing(t) - mocks.client.EXPECT().GetUserKeyRing().Return(testUserKeyring, nil).AnyTimes() - mocks.client.EXPECT().Addresses().Return(pmapi.AddressList{ - {ID: addrID1, Email: addr1, Type: pmapi.OriginalAddress, Receive: true}, - {ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: true}, - }) - mocks.client.EXPECT().ListLabels(gomock.Any()).AnyTimes() - mocks.client.EXPECT().CountMessages(gomock.Any(), "") - - // Call to get latest event ID and then to process first event. - eventAfterSyncRequested := make(chan struct{}) - mocks.client.EXPECT().GetEvent(gomock.Any(), "").Return(&pmapi.Event{ - EventID: "firstEventID", - }, nil) - mocks.client.EXPECT().GetEvent(gomock.Any(), "firstEventID").DoAndReturn(func(_ context.Context, _ string) (*pmapi.Event, error) { - close(eventAfterSyncRequested) - return &pmapi.Event{ - EventID: "latestEventID", - }, nil - }) - - mocks.client.EXPECT().ListMessages(gomock.Any(), gomock.Any()).Return(msgs, len(msgs), nil).AnyTimes() - for _, msg := range msgs { - mocks.client.EXPECT().GetMessage(gomock.Any(), msg.ID).Return(msg, nil).AnyTimes() - } - - var err error - mocks.store, err = New( - nil, // Sentry reporter is not used under unit tests. - mocks.panicHandler, - mocks.user, - mocks.events, - cache.NewInMemoryCache(1<<20), - message.NewBuilder(runtime.NumCPU(), runtime.NumCPU()), - filepath.Join(mocks.tmpDir, "mailbox-test.db"), - mocks.cache, - ) - require.NoError(mocks.tb, err) - - require.NoError(mocks.tb, mocks.store.UnlockCache(testUserKeyring)) - - // We want to wait until first sync has finished. - // Checking that event after sync was reuested is not the best way to - // do the check, because sync could take more time, but sync is going - // in background and if there is no message to wait for, we don't have - // anything better. - select { - case <-eventAfterSyncRequested: - case <-time.After(5 * time.Second): - } - require.Eventually(mocks.tb, func() bool { - for _, msg := range msgs { - _, err := mocks.store.getMessageFromDB(msg.ID) - if err != nil { - // To see in test result the latest error for debugging. - fmt.Println("Sync wait error:", err) - return false - } - } - return true - }, 5*time.Second, 10*time.Millisecond) -} diff --git a/internal/store/store_test_exports.go b/internal/store/store_test_exports.go deleted file mode 100644 index b955b810..00000000 --- a/internal/store/store_test_exports.go +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "encoding/json" - "fmt" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/stretchr/testify/assert" - bolt "go.etcd.io/bbolt" -) - -func (loop *eventLoop) IsRunning() bool { - return loop.isRunning -} - -// TestSync triggers a sync of the store. -func (store *Store) TestSync() { - store.lock.Lock() - defer store.lock.Unlock() - - // Sync can happen any time. Sync assigns sequence numbers and UIDs - // in the order of fetching from the server. We expect in the test - // that sequence numbers and UIDs are assigned in the same order as - // written in scenario setup. With more than one sync that cannot - // be guaranteed so once test calls this function, first it has to - // delete previous any already synced sequence numbers and UIDs. - _ = store.truncateMailboxesBucket() - - store.triggerSync() -} - -// TestPollNow triggers a loop of the event loop. -func (store *Store) TestPollNow() { - store.eventLoop.pollNow() -} - -// TestIsSyncRunning returns whether the sync is currently ongoing. -func (store *Store) TestIsSyncRunning() bool { - return store.isSyncRunning -} - -// TestGetEventLoop returns the store's event loop. -func (store *Store) TestGetEventLoop() *eventLoop { //nolint:revive - return store.eventLoop -} - -// TestGetLastEvent returns last event processed by the store's event loop. -func (store *Store) TestGetLastEvent() *pmapi.Event { - return store.eventLoop.currentEvent -} - -// TestGetStoreFilePath returns the filepath of the store's database file. -func (store *Store) TestGetStoreFilePath() string { - return store.filePath -} - -// TestDumpDB will dump store database content. -func (store *Store) TestDumpDB(tb assert.TestingT) { - if store == nil || store.db == nil { - fmt.Printf(">>>>>>>> NIL STORE / DB <<<<<\n\n") - assert.Fail(tb, "store or database is nil") - return - } - - dumpCounts := true - fmt.Printf(">>>>>>>> DUMP %s <<<<<\n\n", store.db.Path()) - - txMails := txDumpMailsFactory(tb) - - txDump := func(tx *bolt.Tx) error { - if dumpCounts { - if err := txDumpCounts(tx); err != nil { - return err - } - } - return txMails(tx) - } - - assert.NoError(tb, store.db.View(txDump)) -} - -func txDumpMailsFactory(tb assert.TestingT) func(tx *bolt.Tx) error { - return func(tx *bolt.Tx) error { - mailboxes := tx.Bucket(mailboxesBucket) - metadata := tx.Bucket(metadataBucket) - err := mailboxes.ForEach(func(mboxName, mboxData []byte) error { - fmt.Println("mbox:", string(mboxName)) - b := mailboxes.Bucket(mboxName).Bucket(imapIDsBucket) - deletedMailboxes := mailboxes.Bucket(mboxName).Bucket(deletedIDsBucket) - c := b.Cursor() - i := 0 - for imapID, apiID := c.First(); imapID != nil; imapID, apiID = c.Next() { - i++ - isDeleted := deletedMailboxes != nil && deletedMailboxes.Get(apiID) != nil - fmt.Println(" ", i, "imap", btoi(imapID), "api", string(apiID), "isDeleted", isDeleted) - data := metadata.Get(apiID) - if !assert.NotNil(tb, data) { - continue - } - if !assert.NoError(tb, txMailMeta(data, i)) { - continue - } - } - fmt.Println("total:", i) - return nil - }) - return err - } -} - -func txDumpCounts(tx *bolt.Tx) error { - counts := tx.Bucket(countsBucket) - err := counts.ForEach(func(labelID, countsB []byte) error { - defer fmt.Println() - fmt.Printf("counts id: %q ", string(labelID)) - counts := &mailboxCounts{} - if err := json.Unmarshal(countsB, counts); err != nil { - fmt.Printf(" Error %v", err) - return nil - } - fmt.Printf(" total :%d unread %d", counts.TotalOnAPI, counts.UnreadOnAPI) - return nil - }) - return err -} - -func txMailMeta(data []byte, i int) error { - fullMetaDump := false - msg := &pmapi.Message{} - if err := json.Unmarshal(data, msg); err != nil { - return err - } - if msg.Body != "" { - fmt.Printf(" %d body %s\n\n", i, msg.Body) - panic("NONZERO BODY") - } - if i >= 10 { - return nil - } - if fullMetaDump { - fmt.Printf(" %d meta %s\n\n", i, string(data)) - } else { - fmt.Println( - " Subj", msg.Subject, - "\n From", msg.Sender, - "\n Time", msg.Time, - "\n Labels", msg.LabelIDs, - "\n Unread", msg.Unread, - ) - } - - return nil -} diff --git a/internal/store/sync.go b/internal/store/sync.go deleted file mode 100644 index 0c5bb112..00000000 --- a/internal/store/sync.go +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - "math" - "sync" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" -) - -const ( - syncMinPagesPerWorker = 10 - syncMessagesMaxWorkers = 5 - maxFilterPageSize = 150 -) - -type storeSynchronizer interface { - getAllMessageIDs() ([]string, error) - createOrUpdateMessagesEvent([]*pmapi.Message) error - deleteMessagesEvent([]string) error - saveSyncState(finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) -} - -type messageLister interface { - ListMessages(context.Context, *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) -} - -func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api messageLister, syncState *syncState) error { - labelID := pmapi.AllMailLabel - - // When the full sync starts (i.e. is not already in progress), we need to load - // - all message IDs in database, so we can see which messages we need to remove at the end of the sync - // - ID ranges which indicate how to split work into multiple workers - if !syncState.isIncomplete() { - if err := syncState.loadMessageIDsToBeDeleted(); err != nil { - return errors.Wrap(err, "failed to load message IDs") - } - - if err := findIDRanges(labelID, api, syncState); err != nil { - return errors.Wrap(err, "failed to load IDs ranges") - } - syncState.save() - } - - wg := &sync.WaitGroup{} - - shouldStop := 0 // Using integer to have it atomic. - var resultError error - - for _, idRange := range syncState.idRanges { - wg.Add(1) - idRange := idRange // Bind for goroutine. - go func() { - defer panicHandler.HandlePanic() - defer wg.Done() - - err := syncBatch(labelID, store, api, syncState, idRange, &shouldStop) - if err != nil { - shouldStop = 1 - resultError = errors.Wrap(err, "failed to sync group") - } - }() - } - - wg.Wait() - - if resultError == nil { - if err := syncState.deleteMessagesToBeDeleted(); err != nil { - return errors.Wrap(err, "failed to delete messages") - } - } - - return resultError -} - -func findIDRanges(labelID string, api messageLister, syncState *syncState) error { - _, count, err := getSplitIDAndCount(labelID, api, 0) - if err != nil { - return errors.Wrap(err, "failed to get first ID and count") - } - log.WithField("total", count).Debug("Finding ID ranges") - if count == 0 { - return nil - } - - syncState.initIDRanges() - - pages := int(math.Ceil(float64(count) / float64(maxFilterPageSize))) - workers := (pages / syncMinPagesPerWorker) + 1 - if workers > syncMessagesMaxWorkers { - workers = syncMessagesMaxWorkers - } - - if workers == 1 { - return nil - } - - step := int(math.Round(float64(pages) / float64(workers))) - // Increment steps in case there are more steps than max # of workers (due to rounding). - if (step*syncMessagesMaxWorkers)+1 < pages { - step++ - } - - for page := step; page < pages; page += step { - splitID, _, err := getSplitIDAndCount(labelID, api, page) - if err != nil { - return errors.Wrap(err, "failed to get IDs range") - } - // Some messages were probably deleted and so the page does not exist anymore. - // Would be good to start this function again, but let's rather start the sync instead of - // wasting time of many calls to API to find where to split workers. - if splitID == "" { - break - } - syncState.addIDRange(splitID) - } - - return nil -} - -func getSplitIDAndCount(labelID string, api messageLister, page int) (string, int, error) { - sort := "ID" - desc := false - filter := &pmapi.MessagesFilter{ - LabelID: labelID, - Sort: sort, - Desc: &desc, - PageSize: maxFilterPageSize, - Page: page, - Limit: 1, - } - // If the page does not exist, an empty page instead of an error is returned. - messages, total, err := api.ListMessages(context.Background(), filter) - if err != nil { - return "", 0, errors.Wrap(err, "failed to list messages") - } - if len(messages) == 0 { - return "", 0, nil - } - return messages[0].ID, total, nil -} - -func syncBatch( //nolint:funlen - labelID string, - store storeSynchronizer, - api messageLister, - syncState *syncState, - idRange *syncIDRange, - shouldStop *int, -) error { - log.WithField("start", idRange.StartID).WithField("stop", idRange.StopID).Info("Starting sync batch") - for { - if *shouldStop == 1 || idRange.isFinished() { - break - } - - sort := "ID" - desc := true - filter := &pmapi.MessagesFilter{ - LabelID: labelID, - Sort: sort, - Desc: &desc, - PageSize: maxFilterPageSize, - Page: 0, - - // Messages with BeginID and EndID are included. We will process - // those messages twice, but that's OK. - // When message is completely removed, it still works as expected. - BeginID: idRange.StartID, - EndID: idRange.StopID, - } - - log.WithField("begin", filter.BeginID).WithField("end", filter.EndID).Debug("Fetching page") - - messages, _, err := api.ListMessages(context.Background(), filter) - if err != nil { - return errors.Wrap(err, "failed to list messages") - } - - if len(messages) == 0 { - break - } - - for _, m := range messages { - syncState.doNotDeleteMessageID(m.ID) - } - syncState.save() - - if err := store.createOrUpdateMessagesEvent(messages); err != nil { - return errors.Wrap(err, "failed to create or update messages") - } - - pageLastMessageID := messages[len(messages)-1].ID - if !desc { - idRange.setStartID(pageLastMessageID) - } else { - idRange.setStopID(pageLastMessageID) - } - - if len(messages) < maxFilterPageSize { - break - } - } - return nil -} diff --git a/internal/store/sync_state.go b/internal/store/sync_state.go deleted file mode 100644 index 05de0e00..00000000 --- a/internal/store/sync_state.go +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "sync" - "time" - - "github.com/pkg/errors" -) - -type syncState struct { - lock *sync.RWMutex - store storeSynchronizer - - // finishTime is the time, when the sync was finished for the last time. - // When it's zero, it was never finished or the sync is ongoing. - finishTime int64 - - // idRanges are ID ranges which are used to split work in several workers. - // On the beginning of the sync it will find split IDs which are used to - // create this ranges. If we have 10000 messages and five workers, it will - // find IDs around 2000, 4000, 6000 and 8000 and then first worker will - // sync IDs 0-2000, second 2000-4000 and so on. - idRanges []*syncIDRange - - // idsToBeDeletedMap is map with keys as message IDs. On the beginning - // of the sync, it will load all message IDs in database. During the sync, - // it will delete all messages from the map which were sycned. The rest - // at the end of the sync will be removed as those messages were not synced - // again. We do that because we don't want to remove everything on the - // beginning of the sync to keep client synced. - idsToBeDeletedMap map[string]bool -} - -func newSyncState(store storeSynchronizer, finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) *syncState { - idsToBeDeletedMap := map[string]bool{} - for _, id := range idsToBeDeleted { - idsToBeDeletedMap[id] = true - } - - syncState := &syncState{ - lock: &sync.RWMutex{}, - store: store, - - finishTime: finishTime, - idRanges: idRanges, - idsToBeDeletedMap: idsToBeDeletedMap, - } - - for _, idRange := range idRanges { - idRange.syncState = syncState - } - - return syncState -} - -func (s *syncState) save() { - s.lock.Lock() - defer s.lock.Unlock() - - s.store.saveSyncState(s.finishTime, s.idRanges, s.getIDsToBeDeleted()) -} - -// isIncomplete returns whether the sync is in progress (no matter whether -// the sync is running or just not finished by info from database). -func (s *syncState) isIncomplete() bool { - s.lock.Lock() - defer s.lock.Unlock() - - return s.finishTime == 0 && len(s.idRanges) != 0 -} - -// isFinished returns whether the sync was finished. -func (s *syncState) isFinished() bool { - s.lock.Lock() - defer s.lock.Unlock() - - return s.finishTime != 0 -} - -// clearFinishTime sets finish time to zero. -func (s *syncState) clearFinishTime() { - s.lock.Lock() - defer s.save() - defer s.lock.Unlock() - - s.finishTime = 0 -} - -// setFinishTime sets finish time to current time. -func (s *syncState) setFinishTime() { - s.lock.Lock() - defer s.save() - defer s.lock.Unlock() - - s.finishTime = time.Now().UnixNano() -} - -// initIDRanges inits the main full range. Then each range is added -// by `addIDRange`. -func (s *syncState) initIDRanges() { - s.lock.Lock() - defer s.lock.Unlock() - - s.idRanges = []*syncIDRange{{ - syncState: s, - StartID: "", - StopID: "", - }} -} - -// addIDRange sets `splitID` as stopID for last range and adds new one -// starting with `splitID`. -func (s *syncState) addIDRange(splitID string) { - s.lock.Lock() - defer s.lock.Unlock() - - lastGroup := s.idRanges[len(s.idRanges)-1] - lastGroup.StopID = splitID - - s.idRanges = append(s.idRanges, &syncIDRange{ - syncState: s, - StartID: splitID, - StopID: "", - }) -} - -// loadMessageIDsToBeDeleted loads all message IDs from database -// and by default all IDs are meant for deletion. During sync for -// each ID `doNotDeleteMessageID` has to be called to remove that -// message from being deleted by `deleteMessagesToBeDeleted`. -func (s *syncState) loadMessageIDsToBeDeleted() error { - idsToBeDeletedMap := make(map[string]bool) - ids, err := s.store.getAllMessageIDs() - if err != nil { - return err - } - for _, id := range ids { - idsToBeDeletedMap[id] = true - } - - s.lock.Lock() - defer s.save() - defer s.lock.Unlock() - - s.idsToBeDeletedMap = idsToBeDeletedMap - return nil -} - -func (s *syncState) doNotDeleteMessageID(id string) { - s.lock.Lock() - defer s.lock.Unlock() - - delete(s.idsToBeDeletedMap, id) -} - -func (s *syncState) deleteMessagesToBeDeleted() error { - s.lock.Lock() - defer s.lock.Unlock() - - idsToBeDeleted := s.getIDsToBeDeleted() - log.Infof("Deleting %v messages after sync", len(idsToBeDeleted)) - if err := s.store.deleteMessagesEvent(idsToBeDeleted); err != nil { - return errors.Wrap(err, "failed to delete messages") - } - return nil -} - -// getIDsToBeDeleted is helper to convert internal map for easier -// manipulation to array. -func (s *syncState) getIDsToBeDeleted() []string { - keys := []string{} - for key := range s.idsToBeDeletedMap { - keys = append(keys, key) - } - return keys -} - -// syncIDRange holds range which IDs need to be synced. -type syncIDRange struct { - syncState *syncState - StartID string - StopID string -} - -func (r *syncIDRange) setStartID(startID string) { - r.StartID = startID - r.syncState.save() -} - -func (r *syncIDRange) setStopID(stopID string) { - r.StopID = stopID - r.syncState.save() -} - -// isFinished returns syncIDRange is finished when StartID and StopID -// are the same. But it cannot be full range, full range cannot be -// determined in other way than asking API. -func (r *syncIDRange) isFinished() bool { - return r.StartID == r.StopID && r.StartID != "" -} diff --git a/internal/store/sync_state_test.go b/internal/store/sync_state_test.go deleted file mode 100644 index f5180025..00000000 --- a/internal/store/sync_state_test.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "sort" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSyncState_IDRanges(t *testing.T) { - store := newSyncer() - syncState := newSyncState(store, 0, []*syncIDRange{}, []string{}) - - syncState.initIDRanges() - syncState.addIDRange("100") - syncState.addIDRange("200") - - r := syncState.idRanges - assert.Equal(t, "", r[0].StartID) - assert.Equal(t, "100", r[0].StopID) - assert.Equal(t, "100", r[1].StartID) - assert.Equal(t, "200", r[1].StopID) - assert.Equal(t, "200", r[2].StartID) - assert.Equal(t, "", r[2].StopID) -} - -func TestSyncState_IDRangesLoaded(t *testing.T) { - store := newSyncer() - syncState := newSyncState(store, 0, []*syncIDRange{ - {StartID: "", StopID: "100"}, - {StartID: "100", StopID: ""}, - }, []string{}) - - r := syncState.idRanges - assert.Equal(t, "", r[0].StartID) - assert.Equal(t, "100", r[0].StopID) - assert.Equal(t, "100", r[1].StartID) - assert.Equal(t, "", r[1].StopID) -} - -func TestSyncState_IDsToBeDeleted(t *testing.T) { - store := newSyncer() - store.allMessageIDs = generateIDs(1, 9) - - syncState := newSyncState(store, 0, []*syncIDRange{}, []string{}) - - require.Nil(t, syncState.loadMessageIDsToBeDeleted()) - syncState.doNotDeleteMessageID("1") - syncState.doNotDeleteMessageID("2") - syncState.doNotDeleteMessageID("3") - syncState.doNotDeleteMessageID("notthere") - - idsToBeDeleted := syncState.getIDsToBeDeleted() - sort.Strings(idsToBeDeleted) - assert.Equal(t, generateIDs(4, 9), idsToBeDeleted) -} - -func TestSyncState_IDsToBeDeletedLoaded(t *testing.T) { - store := newSyncer() - store.allMessageIDs = generateIDs(1, 9) - - syncState := newSyncState(store, 0, []*syncIDRange{}, generateIDs(4, 9)) - - idsToBeDeleted := syncState.getIDsToBeDeleted() - sort.Strings(idsToBeDeleted) - assert.Equal(t, generateIDs(4, 9), idsToBeDeleted) -} diff --git a/internal/store/sync_test.go b/internal/store/sync_test.go deleted file mode 100644 index 1b13778d..00000000 --- a/internal/store/sync_test.go +++ /dev/null @@ -1,528 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - "sort" - "strconv" - "sync" - "testing" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type mockLister struct { - err error - messageIDs []string -} - -func (m *mockLister) ListMessages(_ context.Context, filter *pmapi.MessagesFilter) (msgs []*pmapi.Message, total int, err error) { - if m.err != nil { - return nil, 0, m.err - } - skipByID := true - skipByPaging := filter.PageSize * filter.Page - for idx := 0; idx < len(m.messageIDs); idx++ { - var messageID string - if !*filter.Desc { - messageID = m.messageIDs[idx] - if filter.BeginID == "" || messageID == filter.BeginID { - skipByID = false - } - } else { - messageID = m.messageIDs[len(m.messageIDs)-1-idx] - if filter.EndID == "" || messageID == filter.EndID { - skipByID = false - } - } - if skipByID { - continue - } - skipByPaging-- - if skipByPaging > 0 { - continue - } - msgs = append(msgs, &pmapi.Message{ - ID: messageID, - }) - if len(msgs) == filter.PageSize || len(msgs) == filter.Limit { - break - } - if !*filter.Desc { - if messageID == filter.EndID { - break - } - } else { - if messageID == filter.BeginID { - break - } - } - } - return msgs, len(m.messageIDs), nil -} - -type mockStoreSynchronizer struct { - locker sync.Locker - allMessageIDs []string - errCreateOrUpdateMessagesEvent error - createdMessageIDsByBatch [][]string -} - -func newSyncer() *mockStoreSynchronizer { - return &mockStoreSynchronizer{ - locker: &sync.Mutex{}, - } -} - -func (m *mockStoreSynchronizer) getAllMessageIDs() ([]string, error) { - m.locker.Lock() - defer m.locker.Unlock() - - return m.allMessageIDs, nil -} - -func (m *mockStoreSynchronizer) createOrUpdateMessagesEvent(messages []*pmapi.Message) error { - m.locker.Lock() - defer m.locker.Unlock() - - if m.errCreateOrUpdateMessagesEvent != nil { - return m.errCreateOrUpdateMessagesEvent - } - createdMessageIDs := []string{} - for _, message := range messages { - createdMessageIDs = append(createdMessageIDs, message.ID) - } - m.createdMessageIDsByBatch = append(m.createdMessageIDsByBatch, createdMessageIDs) - return nil -} - -func (m *mockStoreSynchronizer) deleteMessagesEvent([]string) error { - m.locker.Lock() - defer m.locker.Unlock() - - return nil -} - -func (m *mockStoreSynchronizer) saveSyncState(finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) { - m.locker.Lock() - defer m.locker.Unlock() -} - -func newTestSyncState(store storeSynchronizer, splitIDs ...string) *syncState { - syncState := newSyncState(store, 0, []*syncIDRange{}, []string{}) - syncState.initIDRanges() - for _, splitID := range splitIDs { - syncState.addIDRange(splitID) - } - return syncState -} - -func generateIDs(start, stop int) []string { - ids := []string{} - for x := start; x <= stop; x++ { - ids = append(ids, strconv.Itoa(x)) - } - return ids -} - -func generateIDsR(start, stop int) []string { - ids := []string{} - for x := start; x >= stop; x-- { - ids = append(ids, strconv.Itoa(x)) - } - return ids -} - -// Tests - -func TestSyncAllMail(t *testing.T) { //nolint:funlen - m, clear := initMocks(t) - defer clear() - - numberOfMessages := 10000 - - api := &mockLister{ - messageIDs: generateIDs(1, numberOfMessages), - } - - tests := []struct { - name string - idRanges []*syncIDRange - idsToBeDeleted []string - wantUpdatedIDs []string - wantNotUpdatedIDs []string - }{ - { - "full sync", - []*syncIDRange{}, - []string{}, - api.messageIDs, - []string{}, - }, - { - "continue with interrupted sync", - []*syncIDRange{ - {StartID: "2000", StopID: "2100"}, - {StartID: "4000", StopID: "4200"}, - {StartID: "9500", StopID: ""}, - }, - mergeArrays(generateIDs(2000, 2100), generateIDs(4000, 4200), generateIDs(9500, 10010)), - mergeArrays(generateIDs(2000, 2100), generateIDs(4000, 4200), generateIDs(9500, 10000)), - mergeArrays(generateIDs(1, 1999), generateIDs(2101, 3999), generateIDs(4201, 9459)), - }, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - store := newSyncer() - store.allMessageIDs = generateIDs(1, numberOfMessages+10) - - syncState := newSyncState(store, 0, tc.idRanges, tc.idsToBeDeleted) - - err := syncAllMail(m.panicHandler, store, api, syncState) - require.Nil(t, err) - - // Check all messages were created or updated. - updateMessageIDsMap := map[string]bool{} - for _, messageIDs := range store.createdMessageIDsByBatch { - for _, messageID := range messageIDs { - updateMessageIDsMap[messageID] = true - } - } - for _, messageID := range tc.wantUpdatedIDs { - assert.True(t, updateMessageIDsMap[messageID], "Message %s was not created/updated, but should", messageID) - } - for _, messageID := range tc.wantNotUpdatedIDs { - assert.False(t, updateMessageIDsMap[messageID], "Message %s was created/updated, but shouldn't", messageID) - } - - // Check all messages were deleted. - idsToBeDeleted := syncState.getIDsToBeDeleted() - sort.Strings(idsToBeDeleted) - assert.Equal(t, generateIDs(numberOfMessages+1, numberOfMessages+10), idsToBeDeleted) - }) - } -} - -func mergeArrays(arrays ...[]string) []string { - result := []string{} - for _, array := range arrays { - result = append(result, array...) - } - return result -} - -func TestSyncAllMail_FailedListing(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - numberOfMessages := 10000 - - store := newSyncer() - store.allMessageIDs = generateIDs(1, numberOfMessages+10) - - api := &mockLister{ - err: errors.New("error"), - messageIDs: generateIDs(1, numberOfMessages), - } - syncState := newTestSyncState(store) - - err := syncAllMail(m.panicHandler, store, api, syncState) - require.EqualError(t, err, "failed to sync group: failed to list messages: error") -} - -func TestSyncAllMail_FailedCreateOrUpdateMessage(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - numberOfMessages := 10000 - - store := newSyncer() - store.errCreateOrUpdateMessagesEvent = errors.New("error") - store.allMessageIDs = generateIDs(1, numberOfMessages+10) - - api := &mockLister{ - messageIDs: generateIDs(1, numberOfMessages), - } - syncState := newTestSyncState(store) - - err := syncAllMail(m.panicHandler, store, api, syncState) - require.EqualError(t, err, "failed to sync group: failed to create or update messages: error") -} - -func TestFindIDRanges(t *testing.T) { //nolint:funlen - store := newSyncer() - syncState := newTestSyncState(store) - - tests := []struct { - name string - messageIDs []string - wantBatches [][]string - }{ - { - "1k messages - 1 batch", - generateIDs(1, 1000), - [][]string{ - {"", ""}, - }, - }, - { - "1k messages not starting at 1", - generateIDs(1000, 2000), - [][]string{ - {"", ""}, - }, - }, - { - "2k messages - 2 batches", - generateIDs(1, 2000), - [][]string{ - {"", "1050"}, - {"1050", ""}, - }, - }, - { - "4k messages - 3 batches", - generateIDs(1, 4000), - [][]string{ - {"", "1350"}, - {"1350", "2700"}, - {"2700", ""}, - }, - }, - { - "5k messages - 4 batches", - generateIDs(1, 5000), - [][]string{ - {"", "1350"}, - {"1350", "2700"}, - {"2700", "4050"}, - {"4050", ""}, - }, - }, - { - "10k messages - 5 batches", - generateIDs(1, 10000), - [][]string{ - {"", "2100"}, - {"2100", "4200"}, - {"4200", "6300"}, - {"6300", "8400"}, - {"8400", ""}, - }, - }, - { - "150k messages - 5 batches", - generateIDs(1, 150000), - [][]string{ - {"", "30000"}, - {"30000", "60000"}, - {"60000", "90000"}, - {"90000", "120000"}, - {"120000", ""}, - }, - }, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - api := &mockLister{ - messageIDs: tc.messageIDs, - } - - err := findIDRanges(pmapi.AllMailLabel, api, syncState) - - require.Nil(t, err) - require.Equal(t, len(tc.wantBatches), len(syncState.idRanges)) - for idx, idRange := range syncState.idRanges { - want := tc.wantBatches[idx] - assert.Equal(t, want[0], idRange.StartID, "Start ID for IDs range %d does not match", idx) - assert.Equal(t, want[1], idRange.StopID, "Stop ID for IDs range %d does not match", idx) - } - }) - } -} - -func TestFindIDRanges_FailedListing(t *testing.T) { - store := newSyncer() - api := &mockLister{ - err: errors.New("error"), - } - - syncState := newTestSyncState(store) - - err := findIDRanges(pmapi.AllMailLabel, api, syncState) - require.EqualError(t, err, "failed to get first ID and count: failed to list messages: error") -} - -func TestGetSplitIDAndCount(t *testing.T) { //nolint:funlen - tests := []struct { - name string - err error - messageIDs []string - page int - wantID string - wantTotal int - wantErr string - }{ - { - "1000 messages, first page", - nil, - generateIDs(1, 1000), - 0, - "1", - 1000, - "", - }, - { - "1000 messages, 5th page", - nil, - generateIDs(1, 1000), - 4, - "600", - 1000, - "", - }, - { - "one message, first page", - nil, - []string{"1"}, - 0, - "1", - 1, - "", - }, - { - "no message, first page", - nil, - []string{}, - 0, - "", - 0, - "", - }, - { - "listing error", - errors.New("error"), - generateIDs(1, 1000), - 0, - "", - 0, - "failed to list messages: error", - }, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - api := &mockLister{ - err: tc.err, - messageIDs: tc.messageIDs, - } - - id, total, err := getSplitIDAndCount(pmapi.AllMailLabel, api, tc.page) - - if tc.wantErr == "" { - require.Nil(t, err) - } else { - require.EqualError(t, err, tc.wantErr) - } - assert.Equal(t, tc.wantID, id) - assert.Equal(t, tc.wantTotal, total) - }) - } -} - -func TestSyncBatch(t *testing.T) { - tests := []struct { - name string - batchIdx int - wantCreatedMessageIDsByBatch [][]string - }{ - { - "first-batch", - 0, - [][]string{generateIDsR(200, 51), generateIDsR(51, 1)}, - }, - { - "second-batch", - 1, - [][]string{generateIDsR(400, 251), generateIDsR(251, 200)}, - }, - { - "third-batch", - 2, - [][]string{generateIDsR(600, 451), generateIDsR(451, 400)}, - }, - { - "fourth-batch", - 3, - [][]string{generateIDsR(800, 651), generateIDsR(651, 600)}, - }, - { - "fifth-batch", - 4, - [][]string{generateIDsR(1000, 851), generateIDsR(851, 800)}, - }, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - store := newSyncer() - api := &mockLister{ - messageIDs: generateIDs(1, 1000), - } - - err := testSyncBatch(t, store, api, tc.batchIdx, "200", "400", "600", "800") - require.Nil(t, err) - require.Equal(t, tc.wantCreatedMessageIDsByBatch, store.createdMessageIDsByBatch) - }) - } -} - -func TestSyncBatch_FailedListing(t *testing.T) { - store := newSyncer() - api := &mockLister{ - err: errors.New("error"), - messageIDs: generateIDs(1, 1000), - } - - err := testSyncBatch(t, store, api, 0) - require.EqualError(t, err, "failed to list messages: error") -} - -func TestSyncBatch_FailedCreateOrUpdateMessage(t *testing.T) { - store := newSyncer() - store.errCreateOrUpdateMessagesEvent = errors.New("error") - api := &mockLister{ - messageIDs: generateIDs(1, 1000), - } - - err := testSyncBatch(t, store, api, 0) - require.EqualError(t, err, "failed to create or update messages: error") -} - -func testSyncBatch(t *testing.T, store storeSynchronizer, api messageLister, rangeIdx int, splitIDs ...string) error { //nolint:unparam - syncState := newTestSyncState(store, splitIDs...) - idRange := syncState.idRanges[rangeIdx] - shouldStop := 0 - return syncBatch(pmapi.AllMailLabel, store, api, syncState, idRange, &shouldStop) -} diff --git a/internal/store/types.go b/internal/store/types.go deleted file mode 100644 index b9469e70..00000000 --- a/internal/store/types.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -type PanicHandler interface { - HandlePanic() -} - -// BridgeUser is subset of bridge.User for use by the Store. -type BridgeUser interface { - ID() string - GetAddressID(address string) (string, error) - IsConnected() bool - IsCombinedAddressMode() bool - GetPrimaryAddress() string - GetStoreAddresses() []string - GetClient() pmapi.Client - UpdateUser(context.Context) error - UpdateSpace(*pmapi.User) - CloseAllConnections() - CloseConnection(string) - Logout() error -} diff --git a/internal/store/ulimit_default.go b/internal/store/ulimit_default.go deleted file mode 100644 index cbe3461c..00000000 --- a/internal/store/ulimit_default.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build !windows -// +build !windows - -package store - -import ( - "runtime" - "syscall" -) - -func getCurrentFDLimit() (int, error) { - var limits syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limits) - if err != nil { - return 0, err - } - return int(limits.Cur), nil -} - -func countOpenedFDs(limit int) int { - openedFDs := 0 - - for i := 0; i < limit; i++ { - _, _, err := syscall.Syscall(syscall.SYS_FCNTL, uintptr(i), uintptr(syscall.F_GETFL), 0) - if err == 0 { - openedFDs++ - } - } - - return openedFDs -} - -func isFdCloseToULimit() bool { - if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { - return false - } - - limit, err := getCurrentFDLimit() - if err != nil { - log.WithError(err).Error("Cannot get current FD limit") - return false - } - - openedFDs := countOpenedFDs(limit) - - log. - WithField("noGoroutines", runtime.NumCgoCall()). - WithField("noFDs", openedFDs). - WithField("limitFD", limit). - Info("File descriptor check") - return openedFDs >= int(0.95*float64(limit)) -} diff --git a/internal/store/ulimit_windows.go b/internal/store/ulimit_windows.go deleted file mode 100644 index c6775393..00000000 --- a/internal/store/ulimit_windows.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build windows -// +build windows - -package store - -func isFdCloseToULimit() bool { return false } diff --git a/internal/store/user.go b/internal/store/user.go deleted file mode 100644 index 8c203480..00000000 --- a/internal/store/user.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import "math" - -// UserID returns user ID. -func (store *Store) UserID() string { - return store.user.ID() -} - -// GetSpaceKB returns used and total space in kilo bytes (needed for IMAP -// Quota. Quota is "in units of 1024 octets" (or KB) and PM returns bytes. -func (store *Store) GetSpaceKB() (usedSpace, maxSpace uint32, err error) { - apiUser, err := store.client().CurrentUser(exposeContextForIMAP()) - if err != nil { - return 0, 0, err - } - if apiUser.UsedSpace != nil { - usedSpace = store.toKBandLimit(*apiUser.UsedSpace, usedSpaceType) - } - if apiUser.MaxSpace != nil { - maxSpace = store.toKBandLimit(*apiUser.MaxSpace, maxSpaceType) - } - return -} - -type spaceType string - -const ( - usedSpaceType = spaceType("used") - maxSpaceType = spaceType("max") -) - -func (store *Store) toKBandLimit(n int64, space spaceType) uint32 { - if n < 0 { - log.WithField("space", space).Warning("negative number of bytes") - return uint32(0) - } - n /= 1024 - if n > math.MaxUint32 { - log.WithField("space", space).Warning("too large number of bytes") - return uint32(math.MaxUint32) - } - return uint32(n) -} - -// GetMaxUpload returns max size of message + all attachments in bytes. -func (store *Store) GetMaxUpload() (int64, error) { - apiUser, err := store.client().CurrentUser(exposeContextForIMAP()) - if err != nil { - return 0, err - } - return apiUser.MaxUpload, nil -} diff --git a/internal/store/user_address.go b/internal/store/user_address.go deleted file mode 100644 index 72e9ce3b..00000000 --- a/internal/store/user_address.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "encoding/json" - "fmt" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - bolt "go.etcd.io/bbolt" -) - -// GetAddress returns the store address by given ID. -func (store *Store) GetAddress(addressID string) (*Address, error) { - store.lock.RLock() - defer store.lock.RUnlock() - - storeAddress, ok := store.addresses[addressID] - if !ok { - return nil, fmt.Errorf("addressID %v does not exist", addressID) - } - - return storeAddress, nil -} - -// RebuildMailboxes truncates all mailbox buckets and recreates them from the metadata bucket again. -func (store *Store) RebuildMailboxes() (err error) { - store.lock.Lock() - defer store.lock.Unlock() - - log.WithField("user", store.UserID()).Trace("Truncating mailboxes") - - store.addresses = nil - - if err = store.truncateMailboxesBucket(); err != nil { - log.WithError(err).Error("Could not truncate mailboxes bucket") - return - } - - if err = store.truncateAddressInfoBucket(); err != nil { - log.WithError(err).Error("Could not truncate address info bucket") - return - } - - if err = store.init(false); err != nil { - log.WithError(err).Error("Could not init store") - return - } - - if err := store.increaseMailboxesVersion(); err != nil { - log.WithError(err).Error("Could not increase structure version") - // Do not return here. The truncation was already done and mode - // was changed in DB so we need to sync so that users start to see - // messages and not block other operations. - } - - log.WithField("user", store.UserID()).Trace("Rebuilding mailboxes") - return store.initMailboxesBucket() -} - -// createOrDeleteAddressesEvent creates address objects in the store for each necessary address -// and deletes any address objects that shouldn't be there. -// It doesn't do anything to addresses that are rightfully there. -// It should only be called from the event loop. -func (store *Store) createOrDeleteAddressesEvent() (err error) { - store.lock.Lock() - defer store.lock.Unlock() - - labels, err := store.initCounts() - if err != nil { - return errors.Wrap(err, "failed to initialise label counts") - } - - addrInfo, err := store.GetAddressInfo() - if err != nil { - return errors.Wrap(err, "failed to get addresses and address IDs") - } - - // We need at least one address to continue. - if len(addrInfo) < 1 { - return errors.New("no addresses to initialise") - } - - // If in combined mode, we only need the user's primary address. - if store.addressMode == combinedMode { - addrInfo = addrInfo[:1] - } - - // Go through all addresses that *should* be there. - for _, addr := range addrInfo { - if _, ok := store.addresses[addr.AddressID]; ok { - continue - } - - // This address is missing so we create it. - if err = store.addAddress(addr.Address, addr.AddressID, labels); err != nil { - return errors.Wrap(err, "failed to add address to store") - } - } - - // Go through all addresses that *should not* be there. - for _, addr := range store.addresses { - belongs := false - - for _, a := range addrInfo { - if addr.addressID == a.AddressID { - belongs = true - break - } - } - - if belongs { - continue - } - - delete(store.addresses, addr.addressID) - } - - if err = store.truncateMailboxesBucket(); err != nil { - log.WithError(err).Error("Could not truncate mailboxes bucket") - return - } - - return store.initMailboxesBucket() -} - -// truncateAddressInfoBucket removes the address info bucket. -func (store *Store) truncateAddressInfoBucket() (err error) { - log.Trace("Truncating address info bucket") - - tx := func(tx *bolt.Tx) (err error) { - if err = tx.DeleteBucket(addressInfoBucket); err != nil { - return - } - - if _, err = tx.CreateBucketIfNotExists(addressInfoBucket); err != nil { - return - } - - return - } - - return store.db.Update(tx) -} - -// truncateMailboxesBucket removes the mailboxes bucket. -func (store *Store) truncateMailboxesBucket() (err error) { - log.Trace("Truncating mailboxes bucket") - - tx := func(tx *bolt.Tx) (err error) { - mbs := tx.Bucket(mailboxesBucket) - - return mbs.ForEach(func(addrIDMailbox, _ []byte) (err error) { - addr := mbs.Bucket(addrIDMailbox) - - if err = addr.DeleteBucket(imapIDsBucket); err != nil { - return - } - - if _, err = addr.CreateBucketIfNotExists(imapIDsBucket); err != nil { - return - } - - if err = addr.DeleteBucket(apiIDsBucket); err != nil { - return - } - - if _, err = addr.CreateBucketIfNotExists(apiIDsBucket); err != nil { - return - } - - return - }) - } - - return store.db.Update(tx) -} - -// initMailboxesBucket recreates the mailboxes bucket from the metadata bucket. -func (store *Store) initMailboxesBucket() error { - return store.db.Update(func(tx *bolt.Tx) error { - i := 0 - msgs := []*pmapi.Message{} - - err := tx.Bucket(metadataBucket).ForEach(func(k, v []byte) error { - msg := &pmapi.Message{} - - if err := json.Unmarshal(v, msg); err != nil { - return err - } - msgs = append(msgs, msg) - - // Calling txCreateOrUpdateMessages does some overhead by iterating - // all mailboxes, accessing buckets and so on. It's better to do in - // batches instead of one by one (seconds vs hours for huge accounts). - // Average size of metadata is 1k bytes, sometimes up to 2k bytes. - // 10k messages will take about 20 MB of memory. - i++ - if i%10000 == 0 { - store.log.WithField("i", i).Debug("Init mboxes heartbeat") - - for _, a := range store.addresses { - if err := a.txCreateOrUpdateMessages(tx, msgs); err != nil { - return err - } - } - msgs = []*pmapi.Message{} - } - - return nil - }) - if err != nil { - return err - } - - for _, a := range store.addresses { - if err := a.txCreateOrUpdateMessages(tx, msgs); err != nil { - return err - } - } - - return nil - }) -} diff --git a/internal/store/user_address_info.go b/internal/store/user_address_info.go deleted file mode 100644 index 1a335038..00000000 --- a/internal/store/user_address_info.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "encoding/json" - "strings" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - bolt "go.etcd.io/bbolt" -) - -// AddressInfo is the format of the data held in the addresses bucket in the store. -// It allows us to easily keep an address and its ID together and serialisation/deserialisation to []byte. -type AddressInfo struct { - Address, AddressID string -} - -// GetAddressID returns the ID of the given address. -func (store *Store) GetAddressID(addr string) (id string, err error) { - addrs, err := store.GetAddressInfo() - if err != nil { - return - } - - for _, addrInfo := range addrs { - if strings.EqualFold(addrInfo.Address, addr) { - id = addrInfo.AddressID - return - } - } - - err = errors.New("no such address") - - return -} - -// GetAddressInfo returns information about all addresses owned by the user. -// The first element is the user's primary address and the rest (if present) are aliases. -// It tries to source the information from the store but if the store doesn't yet have it, it -// fetches it from the API and caches it for later. -func (store *Store) GetAddressInfo() (addrs []AddressInfo, err error) { - if addrs, err = store.getAddressInfoFromStore(); err == nil && len(addrs) > 0 { - return - } - - // Store does not have address info yet, need to build it first from API. - addressList := store.client().Addresses() - if addressList == nil { - err = errors.New("addresses unavailable") - store.log.WithError(err).Error("Could not get user addresses from API") - return - } - - if err = store.createOrUpdateAddressInfo(addressList); err != nil { - store.log.WithError(err).Warn("Could not update address IDs in store") - return - } - - return store.getAddressInfoFromStore() -} - -// getAddressIDsByAddressFromStore returns a map from address to addressID for each address owned by the user. -func (store *Store) getAddressInfoFromStore() (addrs []AddressInfo, err error) { - store.log.Debug("Retrieving address info from store") - - tx := func(tx *bolt.Tx) (err error) { - c := tx.Bucket(addressInfoBucket).Cursor() - for index, addrInfoBytes := c.First(); index != nil; index, addrInfoBytes = c.Next() { - var addrInfo AddressInfo - - if err = json.Unmarshal(addrInfoBytes, &addrInfo); err != nil { - store.log.WithError(err).Error("Could not unmarshal address and addressID") - return - } - - addrs = append(addrs, addrInfo) - } - - return - } - - err = store.db.View(tx) - - return -} - -// createOrUpdateAddressInfo updates the store address/addressID bucket to match the given address list. -// The address list supplied is assumed to contain active emails in any order. -// It firstly (and stupidly) deletes the bucket of addresses and then fills it with up to date info. -// This is because a user might delete an address and we don't want old addresses lying around (and finding the -// specific ones to delete is likely not much more efficient than just rebuilding from scratch). -func (store *Store) createOrUpdateAddressInfo(addressList pmapi.AddressList) (err error) { - tx := func(tx *bolt.Tx) error { - if err := tx.DeleteBucket(addressInfoBucket); err != nil { - store.log.WithError(err).Error("Could not delete addressIDs bucket") - return err - } - - if _, err := tx.CreateBucketIfNotExists(addressInfoBucket); err != nil { - store.log.WithError(err).Error("Could not recreate addressIDs bucket") - return err - } - - addrsBucket := tx.Bucket(addressInfoBucket) - - for index, address := range filterAddresses(addressList) { - ib := itob(uint32(index)) - - info, err := json.Marshal(AddressInfo{ - Address: address.Email, - AddressID: address.ID, - }) - if err != nil { - store.log.WithError(err).Error("Could not marshal address and addressID") - return err - } - - if err := addrsBucket.Put(ib, info); err != nil { - store.log.WithError(err).Error("Could not put address and addressID into store") - return err - } - } - - return nil - } - - return store.db.Update(tx) -} - -// filterAddresses filters out inactive addresses and ensures the original address is listed first. -func filterAddresses(addressList pmapi.AddressList) (filteredList pmapi.AddressList) { - for _, address := range addressList { - if !address.Receive { - continue - } - - filteredList = append(filteredList, address) - } - - return -} diff --git a/internal/store/user_mailbox.go b/internal/store/user_mailbox.go deleted file mode 100644 index f7d254b9..00000000 --- a/internal/store/user_mailbox.go +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "fmt" - "strings" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" -) - -// createMailbox creates the mailbox via the API. -// The store mailbox is created later by processing an event. -func (store *Store) createMailbox(name string) error { - defer store.eventLoop.pollNow() - - log.WithField("name", name).Debug("Creating mailbox") - - if store.hasMailbox(name) { - return fmt.Errorf("mailbox %v already exists", name) - } - - color := store.leastUsedColor() - - var exclusive bool - switch { - case strings.HasPrefix(name, UserLabelsPrefix): - name = strings.TrimPrefix(name, UserLabelsPrefix) - exclusive = false - case strings.HasPrefix(name, UserFoldersPrefix): - name = strings.TrimPrefix(name, UserFoldersPrefix) - exclusive = true - default: - // Ideally we would throw an error here, but then Outlook for - // macOS keeps trying to make an IMAP Drafts folder and popping - // up the error to the user. - store.log.WithField("name", name). - Warn("Ignoring creation of new mailbox in IMAP root") - return nil - } - - _, err := store.client().CreateLabel(exposeContextForIMAP(), &pmapi.Label{ - Name: name, - Color: color, - Exclusive: pmapi.Boolean(exclusive), - Type: pmapi.LabelTypeMailBox, - }) - return err -} - -// allAddressesHaveMailbox returns whether each address has a mailbox with the given labelID. -func (store *Store) allAddressesHaveMailbox(labelID string) bool { - store.lock.RLock() - defer store.lock.RUnlock() - - for _, a := range store.addresses { - addressHasMailbox := false - for _, m := range a.mailboxes { - if m.labelID == labelID { - addressHasMailbox = true - break - } - } - if !addressHasMailbox { - return false - } - } - return true -} - -// hasMailbox returns whether there is at least one address which has a mailbox with the given name. -func (store *Store) hasMailbox(name string) bool { - mailbox, _ := store.getMailbox(name) - return mailbox != nil -} - -// getMailbox returns the first mailbox with the given name. -func (store *Store) getMailbox(name string) (*Mailbox, error) { - store.lock.RLock() - defer store.lock.RUnlock() - - for _, a := range store.addresses { - for _, m := range a.mailboxes { - if m.labelName == name { - return m, nil - } - } - } - return nil, fmt.Errorf("mailbox %s does not exist", name) -} - -// leastUsedColor returns the least used color to be used for a newly created folder or label. -func (store *Store) leastUsedColor() string { - store.lock.RLock() - defer store.lock.RUnlock() - - colors := []string{} - for _, a := range store.addresses { - for _, m := range a.mailboxes { - colors = append(colors, m.color) - } - } - - return pmapi.LeastUsedColor(colors) -} - -// updateMailbox updates the mailbox via the API. -// The store mailbox is updated later by processing an event. -func (store *Store) updateMailbox(labelID, newName, color string) error { - defer store.eventLoop.pollNow() - - _, err := store.client().UpdateLabel(exposeContextForIMAP(), &pmapi.Label{ - ID: labelID, - Name: newName, - Color: color, - }) - return err -} - -// deleteMailbox deletes the mailbox via the API. -// The store mailbox is deleted later by processing an event. -func (store *Store) deleteMailbox(labelID, addressID string) error { - defer store.eventLoop.pollNow() - - if pmapi.IsSystemLabel(labelID) { - var err error - switch labelID { - case pmapi.SpamLabel: - err = store.client().EmptyFolder(exposeContextForIMAP(), pmapi.SpamLabel, addressID) - case pmapi.TrashLabel: - err = store.client().EmptyFolder(exposeContextForIMAP(), pmapi.TrashLabel, addressID) - default: - err = fmt.Errorf("cannot empty mailbox %v", labelID) - } - return err - } - return store.client().DeleteLabel(exposeContextForIMAP(), labelID) -} - -func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error { - newLabelIDs := []string{} - for labelID := range affectedLabelIDs { - if pmapi.IsSystemLabel(labelID) || store.allAddressesHaveMailbox(labelID) { - continue - } - newLabelIDs = append(newLabelIDs, labelID) - } - if len(newLabelIDs) == 0 { - return nil - } - - labels, err := store.client().ListLabels(exposeContextForIMAP()) - if err != nil { - return err - } - for _, newLabelID := range newLabelIDs { - for _, label := range labels { - if label.ID != newLabelID { - continue - } - if err := store.createOrUpdateMailboxEvent(label); err != nil { - return err - } - } - } - return nil -} - -// createOrUpdateMailboxEvent creates or updates the mailbox in the store. -// This is called from the event loop. -func (store *Store) createOrUpdateMailboxEvent(label *pmapi.Label) error { - store.lock.Lock() - defer store.lock.Unlock() - - if label.Type != pmapi.LabelTypeMailBox { - return nil - } - - if err := store.createOrUpdateMailboxCountsBuckets([]*pmapi.Label{label}); err != nil { - return errors.Wrap(err, "cannot update counts") - } - - for _, a := range store.addresses { - if err := a.createOrUpdateMailboxEvent(label); err != nil { - return err - } - } - return nil -} - -// deleteMailboxEvent deletes the mailbox in the store. -// This is called from the event loop. -func (store *Store) deleteMailboxEvent(labelID string) error { - store.lock.Lock() - defer store.lock.Unlock() - - if err := store.removeMailboxCount(labelID); err != nil { - log.WithError(err).Warn("Problem to remove mailbox counts while deleting mailbox") - } - - for _, a := range store.addresses { - if err := a.deleteMailboxEvent(labelID); err != nil { - return err - } - } - return nil -} diff --git a/internal/store/user_message.go b/internal/store/user_message.go deleted file mode 100644 index 660e7fb5..00000000 --- a/internal/store/user_message.go +++ /dev/null @@ -1,399 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "bytes" - "encoding/json" - "io" - "net/mail" - "net/textproto" - "strings" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - bolt "go.etcd.io/bbolt" -) - -// CreateDraft creates draft with attachments. -// If `attachedPublicKey` is passed, it's added to attachments. -// Both draft and attachments are encrypted with passed `kr` key. -func (store *Store) CreateDraft( - kr *crypto.KeyRing, - message *pmapi.Message, - attachmentReaders []io.Reader, - attachedPublicKey, - attachedPublicKeyName string, - parentID string, -) (*pmapi.Message, []*pmapi.Attachment, error) { - attachments := store.prepareDraftAttachments(message, attachmentReaders, attachedPublicKey, attachedPublicKeyName) - - if err := encryptDraft(kr, message, attachments); err != nil { - return nil, nil, errors.Wrap(err, "failed to encrypt draft") - } - - if ok, err := store.checkDraftTotalSize(message, attachments); err != nil { - return nil, nil, err - } else if !ok { - return nil, nil, errors.New("message is too large") - } - - draftAction := store.getDraftAction(message) - draft, err := store.client().CreateDraft(exposeContextForSMTP(), message, parentID, draftAction) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to create draft") - } - - // Do poll only when call to API succeeded. - defer store.eventLoop.pollNow() - - createdAttachments := []*pmapi.Attachment{} - for _, att := range attachments { - att.attachment.MessageID = draft.ID - - createdAttachment, err := store.client().CreateAttachment(exposeContextForSMTP(), att.attachment, att.encReader, att.sigReader) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to create attachment") - } - createdAttachments = append(createdAttachments, createdAttachment) - } - - return draft, createdAttachments, nil -} - -type draftAttachment struct { - attachment *pmapi.Attachment - reader io.Reader - sigReader io.Reader - encReader io.Reader -} - -func (store *Store) prepareDraftAttachments( - message *pmapi.Message, - attachmentReaders []io.Reader, - attachedPublicKey, - attachedPublicKeyName string, -) []*draftAttachment { - attachments := []*draftAttachment{} - for idx, attachment := range message.Attachments { - attachments = append(attachments, &draftAttachment{ - attachment: attachment, - reader: attachmentReaders[idx], - }) - } - - message.Attachments = nil - - if attachedPublicKey != "" { - publicKeyAttachment := &pmapi.Attachment{ - Name: attachedPublicKeyName + ".asc", - MIMEType: "application/pgp-keys", - Header: textproto.MIMEHeader{}, - } - attachments = append(attachments, &draftAttachment{ - attachment: publicKeyAttachment, - reader: strings.NewReader(attachedPublicKey), - }) - } - - return attachments -} - -func encryptDraft(kr *crypto.KeyRing, message *pmapi.Message, attachments []*draftAttachment) error { - // Since this is a draft, we don't need to sign it. - if err := message.Encrypt(kr, nil); err != nil { - return errors.Wrap(err, "failed to encrypt message") - } - - for _, att := range attachments { - attachment := att.attachment - attachmentBody, err := io.ReadAll(att.reader) - if err != nil { - return errors.Wrap(err, "failed to read attachment") - } - - r := bytes.NewReader(attachmentBody) - sigReader, err := attachment.DetachedSign(kr, r) - if err != nil { - return errors.Wrap(err, "failed to sign attachment") - } - att.sigReader = sigReader - - r = bytes.NewReader(attachmentBody) - encReader, err := attachment.Encrypt(kr, r) - if err != nil { - return errors.Wrap(err, "failed to encrypt attachment") - } - att.encReader = encReader - - att.reader = nil - } - return nil -} - -func (store *Store) checkDraftTotalSize(message *pmapi.Message, attachments []*draftAttachment) (bool, error) { - maxUpload, err := store.GetMaxUpload() - if err != nil { - return false, err - } - - var attSize int64 - for _, att := range attachments { - b, err := io.ReadAll(att.encReader) - if err != nil { - return false, err - } - attSize += int64(len(b)) - att.encReader = bytes.NewBuffer(b) - } - - return int64(len(message.Body))+attSize <= maxUpload, nil -} - -func (store *Store) getDraftAction(message *pmapi.Message) int { - // If not a reply, must be a forward. - if len(message.Header["In-Reply-To"]) == 0 { - return pmapi.DraftActionForward - } - return pmapi.DraftActionReply -} - -// SendMessage sends the message. -func (store *Store) SendMessage(messageID string, req *pmapi.SendMessageReq) error { - defer store.eventLoop.pollNow() - _, _, err := store.client().SendMessage(exposeContextForSMTP(), messageID, req) - return err -} - -// getAllMessageIDs returns all API IDs of messages in the local database. -func (store *Store) getAllMessageIDs() (apiIDs []string, err error) { - err = store.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket(metadataBucket) - return b.ForEach(func(k, v []byte) error { - apiIDs = append(apiIDs, string(k)) - return nil - }) - }) - return -} - -// getMessageFromDB returns pmapi struct of message by API ID. -func (store *Store) getMessageFromDB(apiID string) (msg *pmapi.Message, err error) { - err = store.db.View(func(tx *bolt.Tx) error { - msg, err = store.txGetMessage(tx, apiID) - return err - }) - - return -} - -func (store *Store) txGetMessage(tx *bolt.Tx, apiID string) (*pmapi.Message, error) { - return store.txGetMessageFromBucket(tx.Bucket(metadataBucket), apiID) -} - -func (store *Store) txGetMessageFromBucket(b *bolt.Bucket, apiID string) (*pmapi.Message, error) { - msgb := b.Get([]byte(apiID)) - if msgb == nil { - return nil, ErrNoSuchAPIID - } - msg := &pmapi.Message{} - if err := json.Unmarshal(msgb, msg); err != nil { - return nil, err - } - return msg, nil -} - -func (store *Store) txPutMessage(metaBucket *bolt.Bucket, onlyMeta *pmapi.Message) error { - b, err := json.Marshal(onlyMeta) - if err != nil { - return errors.Wrap(err, "cannot marshall metadata") - } - err = metaBucket.Put([]byte(onlyMeta.ID), b) - if err != nil { - return errors.Wrap(err, "cannot add to metadata bucket") - } - return nil -} - -// createOrUpdateMessageEvent is helper to create only one message with -// createOrUpdateMessagesEvent. -func (store *Store) createOrUpdateMessageEvent(msg *pmapi.Message) error { - return store.createOrUpdateMessagesEvent([]*pmapi.Message{msg}) -} - -// createOrUpdateMessagesEvent tries to create or update messages in database. -// This function is optimised for insertion of many messages at once. -// It calls createLabelsIfMissing if needed. -func (store *Store) createOrUpdateMessagesEvent(msgs []*pmapi.Message) error { //nolint:funlen - store.log.WithField("msgs", msgs).Trace("Creating or updating messages in the store") - - // Strip non-meta first to reduce memory (no need to keep all old msg ID data during update). - err := store.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket(metadataBucket) - for _, msg := range msgs { - clearNonMetadata(msg) - txUpdateMetadataFromDB(b, msg, store.log) - } - return nil - }) - if err != nil { - return err - } - - affectedLabels := map[string]bool{} - for _, m := range msgs { - for _, l := range m.LabelIDs { - affectedLabels[l] = true - } - } - if err = store.createLabelsIfMissing(affectedLabels); err != nil { - return err - } - - // Updating metadata and mailboxes is not atomic, but this is OK. - // The worst case scenario is we have metadata but not updated mailboxes - // which is OK as without information in mailboxes IMAP we will never ask - // for metadata. Also, when doing the operation again, it will simply - // update the metadata. - // The reason to split is efficiency--it's more memory efficient. - - // Update metadata. - err = store.db.Update(func(tx *bolt.Tx) error { - metaBucket := tx.Bucket(metadataBucket) - for _, msg := range msgs { - err := store.txPutMessage(metaBucket, msg) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return err - } - - // Update mailboxes. - err = store.db.Update(func(tx *bolt.Tx) error { - for _, a := range store.addresses { - if err := a.txCreateOrUpdateMessages(tx, msgs); err != nil { - store.log.WithError(err).Error("cannot update maiboxes") - return errors.Wrap(err, "cannot add to mailboxes bucket") - } - } - return nil - }) - if err != nil { - return err - } - - // Notify the cacher that it should start caching messages. - if cache.IsOnDiskCache(store.cache) { - for _, msg := range msgs { - store.msgCachePool.newJob(msg.ID) - } - } - - return nil -} - -// clearNonMetadata to not allow to store decrypted or encrypted data i.e. body -// and attachments. -func clearNonMetadata(onlyMeta *pmapi.Message) { - onlyMeta.Body = "" - onlyMeta.Attachments = nil -} - -// txUpdateMetadataFromDB changes the onlyMeta data. -// If there is stored message in metaBucket the size, header and MIMEType are -// not changed if already set. To change these: -// * size must be updated by Message.SetSize -// * contentType and header must be updated by bodystructure. -func txUpdateMetadataFromDB(metaBucket *bolt.Bucket, onlyMeta *pmapi.Message, log *logrus.Entry) { - msgb := metaBucket.Get([]byte(onlyMeta.ID)) - if msgb == nil { - return - } - - // It is faster to unmarshal only the needed items. - stored := &struct { - Size int64 - Header string - MIMEType string - }{} - if err := json.Unmarshal(msgb, stored); err != nil { - log.WithError(err). - Error("Fail to unmarshal from DB, metadata will be overwritten") - return - } - - // Keep content type. - onlyMeta.MIMEType = stored.MIMEType - if stored.Header != "" && stored.Header != "(No Header)" { - tmpMsg, err := mail.ReadMessage( - strings.NewReader(stored.Header + "\r\n\r\n"), - ) - if err == nil { - onlyMeta.Header = tmpMsg.Header - } else { - log.WithError(err). - Error("Fail to parse, the header will be overwritten") - } - } -} - -// deleteMessageEvent is helper to delete only one message with deleteMessagesEvent. -func (store *Store) deleteMessageEvent(apiID string) error { - return store.deleteMessagesEvent([]string{apiID}) -} - -// deleteMessagesEvent deletes the message from metadata and all mailbox buckets. -func (store *Store) deleteMessagesEvent(apiIDs []string) error { - for _, messageID := range apiIDs { - if err := store.cache.Rem(store.UserID(), messageID); err != nil { - logrus.WithError(err).Error("Failed to remove message from cache") - } - } - - return store.db.Update(func(tx *bolt.Tx) error { - for _, apiID := range apiIDs { - if err := tx.Bucket(metadataBucket).Delete([]byte(apiID)); err != nil { - return err - } - - for _, a := range store.addresses { - if err := a.txDeleteMessage(tx, apiID); err != nil { - return err - } - } - } - return nil - }) -} - -func (store *Store) isMessageADraft(apiID string) bool { - msg, err := store.getMessageFromDB(apiID) - if err != nil { - store.log.WithError(err).Warn("Cannot decide wheather message is draff") - return false - } - - return msg.IsDraft() -} diff --git a/internal/store/user_message_test.go b/internal/store/user_message_test.go deleted file mode 100644 index d2ee5baa..00000000 --- a/internal/store/user_message_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "io" - "net/mail" - "net/textproto" - "strings" - "testing" - - pkgMsg "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" - bolt "go.etcd.io/bbolt" -) - -func TestGetAllMessageIDs(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true) - - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) - insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel}) - insertMessage(t, m, "msg3", "Test message 3", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) - insertMessage(t, m, "msg4", "Test message 4", addrID1, false, []string{}) - - checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"}) -} - -func TestGetMessageFromDB(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true) - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel}) - - tests := []struct{ msgID, wantErr string }{ - {"msg1", ""}, - {"msg2", "no such api id"}, - } - for _, tc := range tests { - tc := tc - t.Run(tc.msgID, func(t *testing.T) { - msg, err := m.store.getMessageFromDB(tc.msgID) - if tc.wantErr != "" { - require.EqualError(t, err, tc.wantErr) - } else { - require.Nil(t, err) - require.Equal(t, tc.msgID, msg.ID) - } - }) - } -} - -func TestCreateOrUpdateMessageMetadata(t *testing.T) { - r := require.New(t) - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true) - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel}) - - metadata, err := m.store.getMessageFromDB("msg1") - r.NoError(err) - - msg := &Message{msg: metadata, store: m.store, storeMailbox: nil} - - // Check non-meta and calculated data are cleared/empty. - r.Equal("", metadata.Body) - r.Equal([]*pmapi.Attachment(nil), metadata.Attachments) - r.Equal("", metadata.MIMEType) - r.Equal(make(mail.Header), metadata.Header) - - wantHeader, wantSize := putBodystructureAndSizeToDB(m, "msg1") - - // Check cached data. - haveHeader, err := msg.GetMIMEHeader() - r.NoError(err) - r.Equal(wantHeader, haveHeader) - - haveSize, err := msg.GetRFC822Size() - r.NoError(err) - r.Equal(wantSize, haveSize) - - // Check cached data are not overridden by reinsert. - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel}) - - haveHeader, err = msg.GetMIMEHeader() - r.NoError(err) - r.Equal(wantHeader, haveHeader) - - haveSize, err = msg.GetRFC822Size() - r.NoError(err) - r.Equal(wantSize, haveSize) -} - -func TestDeleteMessage(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true) - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel}) - insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel}) - - require.Nil(t, m.store.deleteMessageEvent("msg1")) - - checkAllMessageIDs(t, m, []string{"msg2"}) - checkMailboxMessageIDs(t, m, pmapi.AllMailLabel, []wantID{{"msg2", 2}}) -} - -func insertMessage(t *testing.T, m *mocksForStore, id, subject, sender string, unread bool, labelIDs []string) { //nolint:unparam - require.Nil(t, m.store.createOrUpdateMessageEvent(getTestMessage(id, subject, sender, unread, labelIDs))) -} - -func getTestMessage(id, subject, sender string, unread bool, labelIDs []string) *pmapi.Message { - address := &mail.Address{Address: sender} - return &pmapi.Message{ - ID: id, - Subject: subject, - Unread: pmapi.Boolean(unread), - Sender: address, - Flags: pmapi.FlagReceived, - ToList: []*mail.Address{address}, - LabelIDs: labelIDs, - Body: "body of message", - Attachments: []*pmapi.Attachment{{ - ID: "attachment1", - MessageID: id, - Name: "attachment", - }}, - } -} - -func checkAllMessageIDs(t *testing.T, m *mocksForStore, wantIDs []string) { - allIds, allErr := m.store.getAllMessageIDs() - require.Nil(t, allErr) - require.Equal(t, wantIDs, allIds) -} - -func TestCreateDraftCheckMessageSize(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, false) - m.client.EXPECT().CurrentUser(gomock.Any()).Return(&pmapi.User{ - MaxUpload: 100, // Decrypted message 5 chars, encrypted 500+. - }, nil) - - // Even small body is bloated to at least about 500 chars of basic pgp message. - message := &pmapi.Message{ - Body: strings.Repeat("a", 5), - } - attachmentReaders := []io.Reader{} - _, _, err := m.store.CreateDraft(testPrivateKeyRing, message, attachmentReaders, "", "", "") - - require.EqualError(t, err, "message is too large") -} - -func TestCreateDraftCheckMessageWithAttachmentSize(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, false) - m.client.EXPECT().CurrentUser(gomock.Any()).Return(&pmapi.User{ - MaxUpload: 800, // Decrypted message 5 chars + 5 chars of attachment, encrypted 500+ + 300+. - }, nil) - - // Even small body is bloated to at least about 500 chars of basic pgp message. - message := &pmapi.Message{ - Body: strings.Repeat("a", 5), - Attachments: []*pmapi.Attachment{ - {Name: "name"}, - }, - } - // Even small attachment is bloated to about 300 chars of encrypted text. - attachmentReaders := []io.Reader{ - strings.NewReader(strings.Repeat("b", 5)), - } - _, _, err := m.store.CreateDraft(testPrivateKeyRing, message, attachmentReaders, "", "", "") - - require.EqualError(t, err, "message is too large") -} - -func putBodystructureAndSizeToDB(m *mocksForStore, msgID string) (header textproto.MIMEHeader, size uint32) { - size = uint32(42) - - require.NoError(m.tb, m.store.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(sizeBucket).Put([]byte(msgID), itob(size)) - })) - - header = textproto.MIMEHeader{ - "Key": []string{"value"}, - } - - bs := pkgMsg.BodyStructure{ - "": &pkgMsg.SectionInfo{ - Header: []byte("Key: value\r\n\r\n"), - Start: 0, - BSize: int(size - 11), - Size: int(size), - Lines: 3, - }, - } - - raw, err := bs.Serialize() - require.NoError(m.tb, err) - - require.NoError(m.tb, m.store.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(bodystructureBucket).Put([]byte(msgID), raw) - })) - - return header, size -} diff --git a/internal/store/user_sync.go b/internal/store/user_sync.go deleted file mode 100644 index 54cb65dd..00000000 --- a/internal/store/user_sync.go +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - bolt "go.etcd.io/bbolt" -) - -const ( - syncFinishTimeKey = "sync_state" // The original key was sync_state and we want to keep compatibility. - syncIDRangesKey = "id_ranges" - syncIDsToBeDeletedKey = "ids_to_be_deleted" -) - -// updateCountsFromServer will download and set the counts. -func (store *Store) updateCountsFromServer() error { - counts, err := store.client().CountMessages(context.Background(), "") - if err != nil { - return errors.Wrap(err, "cannot update counts from server") - } - - return store.createOrUpdateOnAPICounts(counts) -} - -// isSynced checks whether DB counts are synced with provided counts from API. -func (store *Store) isSynced(countsOnAPI []*pmapi.MessagesCount) (bool, error) { - store.log.WithField("apiCounts", countsOnAPI).Debug("Checking whether store is synced") - - // IMPORTANT: The countsOnAPI can contain duplicates due to event merge - // (ie one label can be present multiple times). It is important to - // process all counts before checking whether they are synced. - if err := store.createOrUpdateOnAPICounts(countsOnAPI); err != nil { - store.log.WithError(err).Error("Cannot update counts before check sync") - return false, err - } - - allCounts, err := store.getOnAPICounts() - if err != nil { - return false, err - } - - store.lock.Lock() - defer store.lock.Unlock() - - countsAreOK := true - for _, counts := range allCounts { - total, unread := uint(0), uint(0) - for _, address := range store.addresses { - mbox, err := address.getMailboxByID(counts.LabelID) - if err != nil { - return false, errors.Wrapf( - err, - "cannot find mailbox for address %q", - address.addressID, - ) - } - - mboxTot, mboxUnread, _, err := mbox.GetCounts() - if err != nil { - errW := errors.Wrap(err, "cannot count messages") - store.log. - WithError(errW). - WithField("label", counts.LabelID). - WithField("address", address.addressID). - Error("IsSynced failed") - return false, err - } - total += mboxTot - unread += mboxUnread - } - - if total != counts.TotalOnAPI || unread != counts.UnreadOnAPI { - store.log.WithFields(logrus.Fields{ - "label": counts.LabelID, - "db-total": total, - "db-unread": unread, - "api-total": counts.TotalOnAPI, - "api-unread": counts.UnreadOnAPI, - }).Warning("counts differ") - countsAreOK = false - } - } - - return countsAreOK, nil -} - -// triggerSync starts a sync of complete user by syncing All Mail mailbox. -// All Mail mailbox contains all messages, so we download all meta data needed -// to generate any address/mailbox IMAP UIDs. -// Sync state can be in three states: -// - Nothing in database. For example when user logs in for the first time. -// `triggerSync` will start full sync. -// - Database has syncIDRangesKey and syncIDsToBeDeletedKey keys with data. -// Sync is in progress or was interrupted. In later case when, `triggerSync` -// will continue where it left off. -// - Database has only syncStateKey with time when database was last synced. -// `triggerSync` will reset it and start full sync again. -func (store *Store) triggerSync() { - syncState := store.loadSyncState() - - // We first clear the last sync state in case this sync fails. - syncState.clearFinishTime() - - // We don't want sync to block. - go func() { - defer store.panicHandler.HandlePanic() - - store.log.Debug("Store sync triggered") - - store.lock.Lock() - - if store.isSyncRunning { - store.lock.Unlock() - store.log.Info("Store sync is already ongoing") - return - } - - if store.syncCooldown.isTooSoon() { - store.lock.Unlock() - store.log.Info("Skipping sync: store tries to resync too often") - return - } - - store.isSyncRunning = true - store.lock.Unlock() - - defer func() { - store.lock.Lock() - store.isSyncRunning = false - store.lock.Unlock() - }() - - store.log.WithField("isIncomplete", syncState.isIncomplete()).Info("Store sync started") - - err := syncAllMail(store.panicHandler, store, store.client(), syncState) - if err != nil { - log.WithError(err).Error("Store sync failed") - store.syncCooldown.increaseWaitTime() - return - } - - store.syncCooldown.reset() - syncState.setFinishTime() - }() -} - -// isSyncFinished returns whether the database has finished a sync. -func (store *Store) isSyncFinished() (isSynced bool) { - return store.loadSyncState().isFinished() -} - -// loadSyncState loads information about sync from database. -// See `triggerSync` to learn more about possible states. -func (store *Store) loadSyncState() *syncState { - finishTime := int64(0) - idRanges := []*syncIDRange{} - idsToBeDeleted := []string{} - - err := store.db.View(func(tx *bolt.Tx) (err error) { - b := tx.Bucket(syncStateBucket) - - finishTimeByte := b.Get([]byte(syncFinishTimeKey)) - if finishTimeByte != nil { - finishTime, err = strconv.ParseInt(string(finishTimeByte), 10, 64) - if err != nil { - store.log.WithError(err).Error("Failed to unmarshal sync IDs ranges") - } - } - - idRangesData := b.Get([]byte(syncIDRangesKey)) - if idRangesData != nil { - if err := json.Unmarshal(idRangesData, &idRanges); err != nil { - store.log.WithError(err).Error("Failed to unmarshal sync IDs ranges") - } - } - - idsToBeDeletedData := b.Get([]byte(syncIDsToBeDeletedKey)) - if idsToBeDeletedData != nil { - if err := json.Unmarshal(idsToBeDeletedData, &idsToBeDeleted); err != nil { - store.log.WithError(err).Error("Failed to unmarshal sync IDs to be deleted") - } - } - - return - }) - if err != nil { - store.log.WithError(err).Error("Failed to load sync state") - } - - return newSyncState(store, finishTime, idRanges, idsToBeDeleted) -} - -// saveSyncState saves information about sync to database. -// See `triggerSync` to learn more about possible states. -func (store *Store) saveSyncState(finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) { - idRangesData, err := json.Marshal(idRanges) - if err != nil { - store.log.WithError(err).Error("Failed to marshall sync IDs ranges") - } - - idsToBeDeletedData, err := json.Marshal(idsToBeDeleted) - if err != nil { - store.log.WithError(err).Error("Failed to marshall sync IDs to be deleted") - } - - err = store.db.Update(func(tx *bolt.Tx) (err error) { - b := tx.Bucket(syncStateBucket) - if finishTime != 0 { - curTime := []byte(fmt.Sprintf("%v", finishTime)) - if err := b.Put([]byte(syncFinishTimeKey), curTime); err != nil { - return err - } - if err := b.Delete([]byte(syncIDRangesKey)); err != nil { - return err - } - if err := b.Delete([]byte(syncIDsToBeDeletedKey)); err != nil { - return err - } - } else { - if err := b.Delete([]byte(syncFinishTimeKey)); err != nil { - return err - } - if err := b.Put([]byte(syncIDRangesKey), idRangesData); err != nil { - return err - } - if err := b.Put([]byte(syncIDsToBeDeletedKey), idsToBeDeletedData); err != nil { - return err - } - } - return nil - }) - - if err != nil { - store.log.WithError(err).Error("Failed to set sync state") - } -} diff --git a/internal/store/user_sync_test.go b/internal/store/user_sync_test.go deleted file mode 100644 index 278884f2..00000000 --- a/internal/store/user_sync_test.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package store - -import ( - "sort" - "testing" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLoadSaveSyncState(t *testing.T) { - m, clear := initMocks(t) - defer clear() - - m.newStoreNoEvents(t, true) - insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) - insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) - - // Clear everything. - - syncState := m.store.loadSyncState() - syncState.clearFinishTime() - - // Check everything is empty at the beginning. - - syncState = m.store.loadSyncState() - checkSyncStateAfterLoad(t, syncState, false, false, []string{}) - - // Save IDs ranges and check everything is also properly loaded. - - syncState.initIDRanges() - syncState.addIDRange("100") - syncState.addIDRange("200") - syncState.save() - - syncState = m.store.loadSyncState() - checkSyncStateAfterLoad(t, syncState, false, true, []string{}) - - // Save IDs to be deleted and check everything is properly loaded. - - require.Nil(t, syncState.loadMessageIDsToBeDeleted()) - - syncState = m.store.loadSyncState() - checkSyncStateAfterLoad(t, syncState, false, true, []string{"msg1", "msg2"}) - - // Set finish time and check everything is resetted to empty values. - - syncState.setFinishTime() - - syncState = m.store.loadSyncState() - checkSyncStateAfterLoad(t, syncState, true, false, []string{}) -} - -func checkSyncStateAfterLoad(t *testing.T, syncState *syncState, wantIsFinished bool, wantIDRanges bool, wantIDsToBeDeleted []string) { - assert.Equal(t, wantIsFinished, syncState.isFinished()) - - if wantIDRanges { - require.Equal(t, 3, len(syncState.idRanges)) - assert.Equal(t, "", syncState.idRanges[0].StartID) - assert.Equal(t, "100", syncState.idRanges[0].StopID) - assert.Equal(t, "100", syncState.idRanges[1].StartID) - assert.Equal(t, "200", syncState.idRanges[1].StopID) - assert.Equal(t, "200", syncState.idRanges[2].StartID) - assert.Equal(t, "", syncState.idRanges[2].StopID) - } else { - assert.Empty(t, syncState.idRanges) - } - - idsToBeDeleted := syncState.getIDsToBeDeleted() - sort.Strings(idsToBeDeleted) - assert.Equal(t, wantIDsToBeDeleted, idsToBeDeleted) -} diff --git a/internal/transfer/mocks/mocks.go b/internal/transfer/mocks/mocks.go deleted file mode 100644 index 90a43511..00000000 --- a/internal/transfer/mocks/mocks.go +++ /dev/null @@ -1,215 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ProtonMail/proton-bridge/v2/internal/transfer (interfaces: PanicHandler,IMAPClientProvider) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - imap "github.com/emersion/go-imap" - sasl "github.com/emersion/go-sasl" - gomock "github.com/golang/mock/gomock" -) - -// MockPanicHandler is a mock of PanicHandler interface. -type MockPanicHandler struct { - ctrl *gomock.Controller - recorder *MockPanicHandlerMockRecorder -} - -// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler. -type MockPanicHandlerMockRecorder struct { - mock *MockPanicHandler -} - -// NewMockPanicHandler creates a new mock instance. -func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler { - mock := &MockPanicHandler{ctrl: ctrl} - mock.recorder = &MockPanicHandlerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder { - return m.recorder -} - -// HandlePanic mocks base method. -func (m *MockPanicHandler) HandlePanic() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "HandlePanic") -} - -// HandlePanic indicates an expected call of HandlePanic. -func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic)) -} - -// MockIMAPClientProvider is a mock of IMAPClientProvider interface. -type MockIMAPClientProvider struct { - ctrl *gomock.Controller - recorder *MockIMAPClientProviderMockRecorder -} - -// MockIMAPClientProviderMockRecorder is the mock recorder for MockIMAPClientProvider. -type MockIMAPClientProviderMockRecorder struct { - mock *MockIMAPClientProvider -} - -// NewMockIMAPClientProvider creates a new mock instance. -func NewMockIMAPClientProvider(ctrl *gomock.Controller) *MockIMAPClientProvider { - mock := &MockIMAPClientProvider{ctrl: ctrl} - mock.recorder = &MockIMAPClientProviderMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIMAPClientProvider) EXPECT() *MockIMAPClientProviderMockRecorder { - return m.recorder -} - -// Authenticate mocks base method. -func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Authenticate", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Authenticate indicates an expected call of Authenticate. -func (mr *MockIMAPClientProviderMockRecorder) Authenticate(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockIMAPClientProvider)(nil).Authenticate), arg0) -} - -// Capability mocks base method. -func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Capability") - ret0, _ := ret[0].(map[string]bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Capability indicates an expected call of Capability. -func (mr *MockIMAPClientProviderMockRecorder) Capability() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capability", reflect.TypeOf((*MockIMAPClientProvider)(nil).Capability)) -} - -// Fetch mocks base method. -func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// Fetch indicates an expected call of Fetch. -func (mr *MockIMAPClientProviderMockRecorder) Fetch(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).Fetch), arg0, arg1, arg2) -} - -// List mocks base method. -func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.MailboxInfo) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// List indicates an expected call of List. -func (mr *MockIMAPClientProviderMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIMAPClientProvider)(nil).List), arg0, arg1, arg2) -} - -// Login mocks base method. -func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Login", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Login indicates an expected call of Login. -func (mr *MockIMAPClientProviderMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockIMAPClientProvider)(nil).Login), arg0, arg1) -} - -// Select mocks base method. -func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxStatus, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Select", arg0, arg1) - ret0, _ := ret[0].(*imap.MailboxStatus) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Select indicates an expected call of Select. -func (mr *MockIMAPClientProviderMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockIMAPClientProvider)(nil).Select), arg0, arg1) -} - -// State mocks base method. -func (m *MockIMAPClientProvider) State() imap.ConnState { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "State") - ret0, _ := ret[0].(imap.ConnState) - return ret0 -} - -// State indicates an expected call of State. -func (mr *MockIMAPClientProviderMockRecorder) State() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockIMAPClientProvider)(nil).State)) -} - -// Support mocks base method. -func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Support", arg0) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Support indicates an expected call of Support. -func (mr *MockIMAPClientProviderMockRecorder) Support(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Support", reflect.TypeOf((*MockIMAPClientProvider)(nil).Support), arg0) -} - -// SupportAuth mocks base method. -func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SupportAuth", arg0) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SupportAuth indicates an expected call of SupportAuth. -func (mr *MockIMAPClientProviderMockRecorder) SupportAuth(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportAuth", reflect.TypeOf((*MockIMAPClientProvider)(nil).SupportAuth), arg0) -} - -// UidFetch mocks base method. -func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UidFetch", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// UidFetch indicates an expected call of UidFetch. -func (mr *MockIMAPClientProviderMockRecorder) UidFetch(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UidFetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).UidFetch), arg0, arg1, arg2) -} diff --git a/internal/updater/channels.go b/internal/updater/channel.go similarity index 86% rename from internal/updater/channels.go rename to internal/updater/channel.go index 5784c65a..50e9ac96 100644 --- a/internal/updater/channels.go +++ b/internal/updater/channel.go @@ -17,15 +17,15 @@ package updater -// UpdateChannel represents an update channel users can be subscribed to. -type UpdateChannel string +// Channel represents an update channel users can be subscribed to. +type Channel string const ( // StableChannel is the channel all users are subscribed to by default. - StableChannel UpdateChannel = "stable" + StableChannel Channel = "stable" // EarlyChannel is the channel users subscribe to when they enable "Early Access". - EarlyChannel UpdateChannel = "early" + EarlyChannel Channel = "early" ) // DefaultUpdateChannel is the default update channel to subscribe to. diff --git a/internal/updater/errors.go b/internal/updater/errors.go deleted file mode 100644 index 7fd09bfb..00000000 --- a/internal/updater/errors.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package updater - -import "errors" - -var ( - ErrDownloadVerify = errors.New("failed to download or verify the update") - ErrInstall = errors.New("failed to install the update") -) diff --git a/internal/updater/locker.go b/internal/updater/locker.go deleted file mode 100644 index 328de2e4..00000000 --- a/internal/updater/locker.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package updater - -import ( - "sync/atomic" - - "github.com/pkg/errors" -) - -var ErrOperationOngoing = errors.New("the operation is already ongoing") - -// locker is an easy way to ensure we only perform one update at a time. -type locker struct { - ongoing atomic.Value -} - -func newLocker() *locker { - l := &locker{} - - l.ongoing.Store(false) - - return l -} - -func (l *locker) doOnce(fn func() error) error { - if l.ongoing.Load().(bool) { //nolint:forcetypeassert - return ErrOperationOngoing - } - - l.ongoing.Store(true) - defer func() { l.ongoing.Store(false) }() - - return fn() -} diff --git a/internal/updater/locker_test.go b/internal/updater/locker_test.go deleted file mode 100644 index e4151447..00000000 --- a/internal/updater/locker_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package updater - -import ( - "sync" - "testing" - "time" - - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestLocker(t *testing.T) { - l := newLocker() - - assert.NoError(t, l.doOnce(func() error { - return nil - })) -} - -func TestLockerForwardsErrors(t *testing.T) { - l := newLocker() - - assert.Error(t, l.doOnce(func() error { - return errors.New("something went wrong") - })) -} - -func TestLockerAllowsOnlyOneOperation(t *testing.T) { - l := newLocker() - - wg := &sync.WaitGroup{} - - wg.Add(1) - go func() { - assert.NoError(t, l.doOnce(func() error { - time.Sleep(2 * time.Second) - wg.Done() - return nil - })) - }() - - time.Sleep(time.Second) - - err := l.doOnce(func() error { return nil }) - if assert.Error(t, err) { - assert.Equal(t, ErrOperationOngoing, err) - } - - wg.Wait() -} diff --git a/internal/updater/mocks/mocks.go b/internal/updater/mocks/mocks.go new file mode 100644 index 00000000..26f14776 --- /dev/null +++ b/internal/updater/mocks/mocks.go @@ -0,0 +1,90 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ProtonMail/proton-bridge/v2/internal/updater (interfaces: Downloader,Installer) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + io "io" + reflect "reflect" + + semver "github.com/Masterminds/semver/v3" + crypto "github.com/ProtonMail/gopenpgp/v2/crypto" + gomock "github.com/golang/mock/gomock" +) + +// MockDownloader is a mock of Downloader interface. +type MockDownloader struct { + ctrl *gomock.Controller + recorder *MockDownloaderMockRecorder +} + +// MockDownloaderMockRecorder is the mock recorder for MockDownloader. +type MockDownloaderMockRecorder struct { + mock *MockDownloader +} + +// NewMockDownloader creates a new mock instance. +func NewMockDownloader(ctrl *gomock.Controller) *MockDownloader { + mock := &MockDownloader{ctrl: ctrl} + mock.recorder = &MockDownloaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDownloader) EXPECT() *MockDownloaderMockRecorder { + return m.recorder +} + +// DownloadAndVerify mocks base method. +func (m *MockDownloader) DownloadAndVerify(arg0 context.Context, arg1 *crypto.KeyRing, arg2, arg3 string) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DownloadAndVerify", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DownloadAndVerify indicates an expected call of DownloadAndVerify. +func (mr *MockDownloaderMockRecorder) DownloadAndVerify(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadAndVerify", reflect.TypeOf((*MockDownloader)(nil).DownloadAndVerify), arg0, arg1, arg2, arg3) +} + +// MockInstaller is a mock of Installer interface. +type MockInstaller struct { + ctrl *gomock.Controller + recorder *MockInstallerMockRecorder +} + +// MockInstallerMockRecorder is the mock recorder for MockInstaller. +type MockInstallerMockRecorder struct { + mock *MockInstaller +} + +// NewMockInstaller creates a new mock instance. +func NewMockInstaller(ctrl *gomock.Controller) *MockInstaller { + mock := &MockInstaller{ctrl: ctrl} + mock.recorder = &MockInstallerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInstaller) EXPECT() *MockInstallerMockRecorder { + return m.recorder +} + +// InstallUpdate mocks base method. +func (m *MockInstaller) InstallUpdate(arg0 *semver.Version, arg1 io.Reader) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstallUpdate", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// InstallUpdate indicates an expected call of InstallUpdate. +func (mr *MockInstallerMockRecorder) InstallUpdate(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallUpdate", reflect.TypeOf((*MockInstaller)(nil).InstallUpdate), arg0, arg1) +} diff --git a/internal/updater/sync.go b/internal/updater/sync.go index c486083d..9cd59903 100644 --- a/internal/updater/sync.go +++ b/internal/updater/sync.go @@ -21,6 +21,7 @@ import ( "crypto/sha256" "errors" "io" + "io/fs" "os" "path/filepath" @@ -87,7 +88,7 @@ func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) { } for _, removeThis := range delList { - if err = os.RemoveAll(removeThis); err != nil && !os.IsNotExist(err) { + if err = os.RemoveAll(removeThis); err != nil && !errors.Is(err, fs.ErrNotExist) { logrus.Error("remove error ", err) return } @@ -195,7 +196,7 @@ func copyRecursively(srcDir, dstDir string) error { //nolint:funlen return err } } - } else if !os.IsNotExist(err) { + } else if !errors.Is(err, fs.ErrNotExist) { return err } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index df264d06..af4a2d3c 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -1,91 +1,50 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - package updater import ( "bytes" + "context" "encoding/json" + "fmt" "io" "github.com/Masterminds/semver/v3" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) -var ErrManualUpdateRequired = errors.New("manual update is required") +var ( + ErrDownloadVerify = errors.New("failed to download or verify the update") + ErrInstall = errors.New("failed to install the update") +) + +type Downloader interface { + DownloadAndVerify(ctx context.Context, kr *crypto.KeyRing, url, sig string) ([]byte, error) +} type Installer interface { InstallUpdate(*semver.Version, io.Reader) error } -type Settings interface { - Get(settings.Key) string - Set(settings.Key, string) - GetFloat64(settings.Key) float64 -} - type Updater struct { - cm pmapi.Manager installer Installer - settings Settings - kr *crypto.KeyRing - - curVer *semver.Version - updateURLName string - platform string - - locker *locker + verifier *crypto.KeyRing + product string + platform string } -func New( - cm pmapi.Manager, - installer Installer, - s Settings, - kr *crypto.KeyRing, - curVer *semver.Version, - updateURLName, platform string, -) *Updater { - // If there's some unexpected value in the preferences, we force it back onto the default channel. - // This prevents users from screwing up silent updates by modifying their prefs.json file. - if channel := UpdateChannel(s.Get(settings.UpdateChannelKey)); !(channel == StableChannel || channel == EarlyChannel) { - s.Set(settings.UpdateChannelKey, string(DefaultUpdateChannel)) - } - +func NewUpdater(installer Installer, verifier *crypto.KeyRing, product, platform string) *Updater { return &Updater{ - cm: cm, - installer: installer, - settings: s, - kr: kr, - curVer: curVer, - updateURLName: updateURLName, - platform: platform, - locker: newLocker(), + installer: installer, + verifier: verifier, + product: product, + platform: platform, } } -func (u *Updater) Check() (VersionInfo, error) { - logrus.Info("Checking for updates") - - b, err := u.cm.DownloadAndVerify( - u.kr, +func (u *Updater) GetVersionInfo(downloader Downloader, channel Channel) (VersionInfo, error) { + b, err := downloader.DownloadAndVerify( + context.Background(), + u.verifier, u.getVersionFileURL(), u.getVersionFileURL()+".sig", ) @@ -99,7 +58,7 @@ func (u *Updater) Check() (VersionInfo, error) { return VersionInfo{}, err } - version, ok := versionMap[u.settings.Get(settings.UpdateChannelKey)] + version, ok := versionMap[channel] if !ok { return VersionInfo{}, errors.New("no updates available for this channel") } @@ -107,45 +66,27 @@ func (u *Updater) Check() (VersionInfo, error) { return version, nil } -func (u *Updater) IsUpdateApplicable(version VersionInfo) bool { - if !version.Version.GreaterThan(u.curVer) { - return false +func (u *Updater) InstallUpdate(downloader Downloader, update VersionInfo) error { + b, err := downloader.DownloadAndVerify( + context.Background(), + u.verifier, + update.Package, + update.Package+".sig", + ) + if err != nil { + return ErrDownloadVerify } - if u.settings.GetFloat64(settings.RolloutKey) > version.RolloutProportion { - return false + if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil { + return ErrInstall } - return true + return nil } -func (u *Updater) IsDowngrade(version VersionInfo) bool { - return version.Version.LessThan(u.curVer) -} - -func (u *Updater) CanInstall(version VersionInfo) bool { - if version.MinAuto == nil { - return true - } - - return !u.curVer.LessThan(version.MinAuto) -} - -func (u *Updater) InstallUpdate(update VersionInfo) error { - return u.locker.doOnce(func() error { - logrus.WithField("package", update.Package).Info("Installing update package") - - b, err := u.cm.DownloadAndVerify(u.kr, update.Package, update.Package+".sig") - if err != nil { - return errors.Wrap(ErrDownloadVerify, err.Error()) - } - - if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil { - return errors.Wrap(ErrInstall, err.Error()) - } - - u.curVer = update.Version - - return nil - }) +// getVersionFileURL returns the URL of the version file. +// For example: +// - https://protonmail.com/download/bridge/version_linux.json +func (u *Updater) getVersionFileURL() string { + return fmt.Sprintf("%v/%v/version_%v.json", Host, u.product, u.platform) } diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go deleted file mode 100644 index bbcca1c5..00000000 --- a/internal/updater/updater_test.go +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package updater - -import ( - "encoding/json" - "errors" - "io" - "os" - "sync" - "testing" - "time" - - "github.com/Masterminds/semver/v3" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi/mocks" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCheck(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - cm := mocks.NewMockManager(c) - - updater := newTestUpdater(cm, "1.1.0", false) - - versionMap := VersionMap{ - "stable": VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.4.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - RolloutProportion: 1.0, - }, - } - - cm.EXPECT().DownloadAndVerify( - gomock.Any(), - updater.getVersionFileURL(), - updater.getVersionFileURL()+".sig", - ).Return(mustMarshal(t, versionMap), nil) - - version, err := updater.Check() - - assert.Equal(t, semver.MustParse("1.5.0"), version.Version) - assert.NoError(t, err) -} - -func TestCheckEarlyAccess(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - cm := mocks.NewMockManager(c) - - updater := newTestUpdater(cm, "1.1.0", true) - - versionMap := VersionMap{ - "stable": VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.0.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - RolloutProportion: 1.0, - }, - "early": VersionInfo{ - Version: semver.MustParse("1.6.0"), - MinAuto: semver.MustParse("1.0.0"), - Package: "https://protonmail.com/download/bridge/update_1.6.0_linux.tgz", - RolloutProportion: 1.0, - }, - } - - cm.EXPECT().DownloadAndVerify( - gomock.Any(), - updater.getVersionFileURL(), - updater.getVersionFileURL()+".sig", - ).Return(mustMarshal(t, versionMap), nil) - - version, err := updater.Check() - - assert.Equal(t, semver.MustParse("1.6.0"), version.Version) - assert.NoError(t, err) -} - -func TestCheckBadSignature(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - cm := mocks.NewMockManager(c) - - updater := newTestUpdater(cm, "1.2.0", false) - - cm.EXPECT().DownloadAndVerify( - gomock.Any(), - updater.getVersionFileURL(), - updater.getVersionFileURL()+".sig", - ).Return(nil, errors.New("bad signature")) - - _, err := updater.Check() - - assert.Error(t, err) -} - -func TestIsUpdateApplicable(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - cm := mocks.NewMockManager(c) - - updater := newTestUpdater(cm, "1.4.0", false) - - versionOld := VersionInfo{ - Version: semver.MustParse("1.3.0"), - MinAuto: semver.MustParse("1.3.0"), - Package: "https://protonmail.com/download/bridge/update_1.3.0_linux.tgz", - RolloutProportion: 1.0, - } - - assert.Equal(t, false, updater.IsUpdateApplicable(versionOld)) - - versionEqual := VersionInfo{ - Version: semver.MustParse("1.4.0"), - MinAuto: semver.MustParse("1.3.0"), - Package: "https://protonmail.com/download/bridge/update_1.4.0_linux.tgz", - RolloutProportion: 1.0, - } - - assert.Equal(t, false, updater.IsUpdateApplicable(versionEqual)) - - versionNew := VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.3.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - RolloutProportion: 1.0, - } - - assert.Equal(t, true, updater.IsUpdateApplicable(versionNew)) -} - -func TestCanInstall(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - cm := mocks.NewMockManager(c) - - updater := newTestUpdater(cm, "1.4.0", false) - - versionManual := VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.5.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - RolloutProportion: 1.0, - } - - assert.Equal(t, false, updater.CanInstall(versionManual)) - - versionAuto := VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.3.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - RolloutProportion: 1.0, - } - - assert.Equal(t, true, updater.CanInstall(versionAuto)) -} - -func TestInstallUpdate(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - cm := mocks.NewMockManager(c) - - updater := newTestUpdater(cm, "1.4.0", false) - - latestVersion := VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.4.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - RolloutProportion: 1.0, - } - - cm.EXPECT().DownloadAndVerify( - gomock.Any(), - latestVersion.Package, - latestVersion.Package+".sig", - ).Return([]byte("tgz_data_here"), nil) - - err := updater.InstallUpdate(latestVersion) - - assert.NoError(t, err) -} - -func TestInstallUpdateBadSignature(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - cm := mocks.NewMockManager(c) - - updater := newTestUpdater(cm, "1.4.0", false) - - latestVersion := VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.4.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - RolloutProportion: 1.0, - } - - cm.EXPECT().DownloadAndVerify( - gomock.Any(), - latestVersion.Package, - latestVersion.Package+".sig", - ).Return(nil, errors.New("bad signature")) - - err := updater.InstallUpdate(latestVersion) - - assert.Error(t, err) -} - -func TestInstallUpdateAlreadyOngoing(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - cm := mocks.NewMockManager(c) - - updater := newTestUpdater(cm, "1.4.0", false) - - updater.installer = &fakeInstaller{delay: 2 * time.Second} - - latestVersion := VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.4.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - RolloutProportion: 1.0, - } - - cm.EXPECT().DownloadAndVerify( - gomock.Any(), - latestVersion.Package, - latestVersion.Package+".sig", - ).Return([]byte("tgz_data_here"), nil) - - wg := &sync.WaitGroup{} - - wg.Add(1) - go func() { - assert.NoError(t, updater.InstallUpdate(latestVersion)) - wg.Done() - }() - - // Wait for the installation to begin. - time.Sleep(time.Second) - - err := updater.InstallUpdate(latestVersion) - if assert.Error(t, err) { - assert.Equal(t, ErrOperationOngoing, err) - } - - wg.Wait() -} - -func newTestUpdater(manager pmapi.Manager, curVer string, earlyAccess bool) *Updater { - return New( - manager, - &fakeInstaller{}, - newFakeSettings(0.5, earlyAccess), - nil, - semver.MustParse(curVer), - "bridge", "linux", - ) -} - -type fakeInstaller struct { - bad bool - delay time.Duration -} - -func (i *fakeInstaller) InstallUpdate(version *semver.Version, r io.Reader) error { - if i.bad { - return errors.New("bad install") - } - - time.Sleep(i.delay) - - return nil -} - -func mustMarshal(t *testing.T, v interface{}) []byte { - b, err := json.Marshal(v) - require.NoError(t, err) - - return b -} - -type fakeSettings struct { - *settings.Settings -} - -// newFakeSettings creates a temporary folder for files. -func newFakeSettings(rollout float64, earlyAccess bool) *fakeSettings { - dir, err := os.MkdirTemp("", "test-settings") - if err != nil { - panic(err) - } - - s := &fakeSettings{Settings: settings.New(dir)} - - s.SetFloat64(settings.RolloutKey, rollout) - - if earlyAccess { - s.Set(settings.UpdateChannelKey, string(EarlyChannel)) - } else { - s.Set(settings.UpdateChannelKey, string(StableChannel)) - } - - return s -} diff --git a/internal/updater/version.go b/internal/updater/version.go index adf1b1df..22a184e1 100644 --- a/internal/updater/version.go +++ b/internal/updater/version.go @@ -18,8 +18,6 @@ package updater import ( - "fmt" - "github.com/Masterminds/semver/v3" ) @@ -80,12 +78,5 @@ type VersionInfo struct { // ... // } // }. -type VersionMap map[string]VersionInfo -// getVersionFileURL returns the URL of the version file. -// For example: -// - https://proton.me/download/bridge/version_linux.json -// - https://proton.me/download/ie/version_linux.json -func (u *Updater) getVersionFileURL() string { - return fmt.Sprintf("%v/%v/version_%v.json", Host, u.updateURLName, u.platform) -} +type VersionMap map[Channel]VersionInfo diff --git a/internal/user/builder.go b/internal/user/builder.go new file mode 100644 index 00000000..ecaf4140 --- /dev/null +++ b/internal/user/builder.go @@ -0,0 +1,61 @@ +package user + +import ( + "context" + + "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" + "gitlab.protontech.ch/go/liteapi" +) + +type request struct { + messageID 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 getMessageCreatedUpdate(msg, literal) + }) + + return msgPool +} diff --git a/internal/user/crypto.go b/internal/user/crypto.go new file mode 100644 index 00000000..f9a2c38d --- /dev/null +++ b/internal/user/crypto.go @@ -0,0 +1,30 @@ +package user + +import ( + "github.com/ProtonMail/gopenpgp/v2/crypto" + "gitlab.protontech.ch/go/liteapi" +) + +func unlockKeyRings(user liteapi.User, addresses []liteapi.Address, keyPass []byte) (*crypto.KeyRing, map[string]*crypto.KeyRing, error) { + userKR, err := user.Keys.Unlock(keyPass, nil) + if err != nil { + return nil, nil, err + } + + addrKRs := make(map[string]*crypto.KeyRing) + + for _, address := range addresses { + if !address.HasKeys.Bool() { + continue + } + + addrKR, err := address.Keys.Unlock(keyPass, userKR) + if err != nil { + return nil, nil, err + } + + addrKRs[address.ID] = addrKR + } + + return userKR, addrKRs, nil +} diff --git a/internal/user/errors.go b/internal/user/errors.go new file mode 100644 index 00000000..6bfdba3d --- /dev/null +++ b/internal/user/errors.go @@ -0,0 +1,12 @@ +package user + +import "errors" + +var ( + ErrNoSuchAddress = errors.New("no such address") + ErrNotImplemented = errors.New("not implemented") + ErrNotSupported = errors.New("not supported") + ErrInvalidReturnPath = errors.New("invalid return path") + ErrInvalidRecipient = errors.New("invalid recipient") + ErrMissingAddressKey = errors.New("missing address key") +) diff --git a/internal/user/events.go b/internal/user/events.go new file mode 100644 index 00000000..da34f20a --- /dev/null +++ b/internal/user/events.go @@ -0,0 +1,230 @@ +package user + +import ( + "context" + + "github.com/ProtonMail/gluon/imap" + "github.com/ProtonMail/proton-bridge/v2/internal/events" + "github.com/bradenaw/juniper/xslices" + "gitlab.protontech.ch/go/liteapi" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// handleAPIEvent handles the given liteapi.Event. +func (user *User) handleAPIEvent(event liteapi.Event) error { + if event.User != nil { + if err := user.handleUserEvent(*event.User); err != nil { + return err + } + } + + if len(event.Addresses) > 0 { + if err := user.handleAddressEvents(event.Addresses); err != nil { + return err + } + } + + if event.MailSettings != nil { + if err := user.handleMailSettingsEvent(*event.MailSettings); err != nil { + return err + } + } + + if len(event.Labels) > 0 { + if err := user.handleLabelEvents(event.Labels); err != nil { + return err + } + } + + if len(event.Messages) > 0 { + if err := user.handleMessageEvents(event.Messages); err != nil { + return err + } + } + + return nil +} + +// handleUserEvent handles the given user event. +func (user *User) handleUserEvent(userEvent liteapi.User) error { + userKR, err := userEvent.Keys.Unlock(user.vault.KeyPass(), nil) + if err != nil { + return err + } + + user.apiUser = userEvent + + user.userKR = userKR + + user.notifyCh <- events.UserChanged{ + UserID: user.ID(), + } + + return nil +} + +// handleAddressEvents handles the given address events. +// TODO: If split address mode, need to signal back to bridge to update the addresses! +func (user *User) handleAddressEvents(addressEvents []liteapi.AddressEvent) error { + for _, event := range addressEvents { + switch event.Action { + case liteapi.EventDelete: + address, err := user.deleteAddress(event.ID) + if err != nil { + return err + } + + // TODO: This is not the same as addressChangedLogout event! + // That was only relevant in split mode. This is used differently now. + user.notifyCh <- events.UserAddressDeleted{ + UserID: user.ID(), + Address: address.Email, + } + + case liteapi.EventCreate: + if err := user.createAddress(event.Address); err != nil { + return err + } + + user.notifyCh <- events.UserAddressCreated{ + UserID: user.ID(), + Address: event.Address.Email, + } + + case liteapi.EventUpdate: + if err := user.updateAddress(event.Address); err != nil { + return err + } + + user.notifyCh <- events.UserAddressChanged{ + UserID: user.ID(), + Address: event.Address.Email, + } + } + } + + return nil +} + +// createAddress creates the given address. +func (user *User) createAddress(address liteapi.Address) error { + addrKR, err := address.Keys.Unlock(user.vault.KeyPass(), user.userKR) + if err != nil { + return err + } + + if user.imapConn != nil { + user.imapConn.addAddress(address.Email) + } + + user.addresses = append(user.addresses, address) + + user.addrKRs[address.ID] = addrKR + + return nil +} + +// updateAddress updates the given address. +func (user *User) updateAddress(address liteapi.Address) error { + if _, err := user.deleteAddress(address.ID); err != nil { + return err + } + + return user.createAddress(address) +} + +// deleteAddress deletes the given address. +func (user *User) deleteAddress(addressID string) (liteapi.Address, error) { + idx := xslices.IndexFunc(user.addresses, func(address liteapi.Address) bool { + return address.ID == addressID + }) + + if idx < 0 { + return liteapi.Address{}, ErrNoSuchAddress + } + + if user.imapConn != nil { + user.imapConn.remAddress(user.addresses[idx].Email) + } + + var address liteapi.Address + + address, user.addresses = user.addresses[idx], append(user.addresses[:idx], user.addresses[idx+1:]...) + + delete(user.addrKRs, addressID) + + return address, nil +} + +// handleMailSettingsEvent handles the given mail settings event. +func (user *User) handleMailSettingsEvent(mailSettingsEvent liteapi.MailSettings) error { + user.settings = mailSettingsEvent + return nil +} + +// handleLabelEvents handles the given label events. +func (user *User) handleLabelEvents(labelEvents []liteapi.LabelEvent) error { + for _, event := range labelEvents { + switch event.Action { + case liteapi.EventDelete: + user.updateCh <- imap.NewMailboxDeleted(imap.LabelID(event.ID)) + + case liteapi.EventCreate: + user.updateCh <- newMailboxCreatedUpdate(imap.LabelID(event.ID), getMailboxName(event.Label)) + + case liteapi.EventUpdate, liteapi.EventUpdateFlags: + user.updateCh <- imap.NewMailboxUpdated(imap.LabelID(event.ID), getMailboxName(event.Label)) + } + } + + return nil +} + +// handleMessageEvents handles the given message events. +func (user *User) handleMessageEvents(messageEvents []liteapi.MessageEvent) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + for _, event := range messageEvents { + switch event.Action { + case liteapi.EventDelete: + return ErrNotImplemented + + case liteapi.EventCreate: + messages, err := user.builder.ProcessAll(ctx, []request{{event.ID, user.addrKRs[event.Message.AddressID]}}) + if err != nil { + return err + } + + user.updateCh <- imap.NewMessagesCreated(maps.Values(messages)...) + + case liteapi.EventUpdate, liteapi.EventUpdateFlags: + user.updateCh <- imap.NewMessageLabelsUpdated( + imap.MessageID(event.ID), + imapLabelIDs(filterLabelIDs(event.Message.LabelIDs)), + !event.Message.Unread.Bool(), + slices.Contains(event.Message.LabelIDs, liteapi.StarredLabel), + ) + } + } + + return nil +} + +func getMailboxName(label liteapi.Label) []string { + var name []string + + switch label.Type { + case liteapi.LabelTypeFolder: + name = []string{folderPrefix, label.Name} + + case liteapi.LabelTypeLabel: + name = []string{labelPrefix, label.Name} + + default: + name = []string{label.Name} + } + + return name +} diff --git a/internal/user/imap.go b/internal/user/imap.go new file mode 100644 index 00000000..fdd2b939 --- /dev/null +++ b/internal/user/imap.go @@ -0,0 +1,293 @@ +package user + +import ( + "context" + "crypto/subtle" + "fmt" + "strings" + "time" + + "github.com/ProtonMail/gluon/imap" + "github.com/bradenaw/juniper/xslices" + "gitlab.protontech.ch/go/liteapi" + "golang.org/x/exp/slices" +) + +var ( + defaultFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted) + defaultPermanentFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted) + defaultAttributes = imap.NewFlagSet() +) + +const ( + folderPrefix = "Folders" + labelPrefix = "Labels" +) + +type imapConnector struct { + client *liteapi.Client + updateCh <-chan imap.Update + + addresses []string + password string + + flags, permFlags, attrs imap.FlagSet +} + +func newIMAPConnector( + client *liteapi.Client, + updateCh <-chan imap.Update, + addresses []string, + password string, +) *imapConnector { + return &imapConnector{ + client: client, + updateCh: updateCh, + + addresses: addresses, + password: password, + + flags: defaultFlags, + permFlags: defaultPermanentFlags, + attrs: defaultAttributes, + } +} + +// Authorize returns whether the given username/password combination are valid for this connector. +func (conn *imapConnector) Authorize(username string, password string) bool { + if subtle.ConstantTimeCompare([]byte(conn.password), []byte(password)) != 1 { + return false + } + + return xslices.IndexFunc(conn.addresses, func(address string) bool { + return strings.EqualFold(address, username) + }) >= 0 +} + +// GetLabel returns information about the label with the given ID. +func (conn *imapConnector) GetLabel(ctx context.Context, labelID imap.LabelID) (imap.Mailbox, error) { + label, err := conn.client.GetLabel(ctx, string(labelID), liteapi.LabelTypeLabel, liteapi.LabelTypeFolder) + if err != nil { + return imap.Mailbox{}, err + } + + var name []string + + switch label.Type { + case liteapi.LabelTypeLabel: + name = []string{labelPrefix, label.Name} + + case liteapi.LabelTypeFolder: + name = []string{folderPrefix, label.Name} + + default: + name = []string{label.Name} + } + + return imap.Mailbox{ + ID: imap.LabelID(label.ID), + Name: name, + Flags: conn.flags, + PermanentFlags: conn.permFlags, + Attributes: conn.attrs, + }, nil +} + +// CreateLabel creates a label with the given name. +func (conn *imapConnector) CreateLabel(ctx context.Context, name []string) (imap.Mailbox, error) { + if len(name) != 2 { + panic("subfolders are unsupported") + } + + var labelType liteapi.LabelType + + if name[0] == folderPrefix { + labelType = liteapi.LabelTypeFolder + } else { + labelType = liteapi.LabelTypeLabel + } + + label, err := conn.client.CreateLabel(ctx, liteapi.CreateLabelReq{ + Name: name[1:][0], + Color: "#f66", + Type: labelType, + }) + if err != nil { + return imap.Mailbox{}, err + } + + return imap.Mailbox{ + ID: imap.LabelID(label.ID), + Name: name, + Flags: conn.flags, + PermanentFlags: conn.permFlags, + Attributes: conn.attrs, + }, nil +} + +// UpdateLabel sets the name of the label with the given ID. +func (conn *imapConnector) UpdateLabel(ctx context.Context, labelID imap.LabelID, newName []string) error { + if len(newName) != 2 { + panic("subfolders are unsupported") + } + + label, err := conn.client.GetLabel(ctx, string(labelID), liteapi.LabelTypeLabel, liteapi.LabelTypeFolder) + if err != nil { + return err + } + + switch label.Type { + case liteapi.LabelTypeFolder: + if newName[0] != folderPrefix { + return fmt.Errorf("cannot rename folder to label") + } + + case liteapi.LabelTypeLabel: + if newName[0] != labelPrefix { + return fmt.Errorf("cannot rename label to folder") + } + + case liteapi.LabelTypeSystem: + return fmt.Errorf("cannot rename system label %q", label.Name) + } + + if _, err := conn.client.UpdateLabel(ctx, label.ID, liteapi.UpdateLabelReq{ + Name: newName[1:][0], + Color: label.Color, + }); err != nil { + return err + } + + return nil +} + +// DeleteLabel deletes the label with the given ID. +func (conn *imapConnector) DeleteLabel(ctx context.Context, labelID imap.LabelID) error { + return conn.client.DeleteLabel(ctx, string(labelID)) +} + +// GetMessage returns the message with the given ID. +func (conn *imapConnector) GetMessage(ctx context.Context, messageID imap.MessageID) (imap.Message, []imap.LabelID, error) { + message, err := conn.client.GetMessage(ctx, string(messageID)) + if err != nil { + return imap.Message{}, nil, err + } + + flags := imap.NewFlagSet() + + if !message.Unread.Bool() { + flags = flags.Add(imap.FlagSeen) + } + + if slices.Contains(message.LabelIDs, liteapi.StarredLabel) { + flags = flags.Add(imap.FlagFlagged) + } + + return imap.Message{ + ID: imap.MessageID(message.ID), + Flags: flags, + Date: time.Unix(message.Time, 0), + }, imapLabelIDs(message.LabelIDs), nil +} + +// CreateMessage creates a new message on the remote. +func (conn *imapConnector) CreateMessage( + ctx context.Context, + labelID imap.LabelID, + literal []byte, + parsedMessage *imap.ParsedMessage, + flags imap.FlagSet, + date time.Time, +) (imap.Message, error) { + return imap.Message{}, ErrNotImplemented +} + +// LabelMessages labels the given messages with the given label ID. +func (conn *imapConnector) LabelMessages(ctx context.Context, messageIDs []imap.MessageID, labelID imap.LabelID) error { + return conn.client.LabelMessages(ctx, strMessageIDs(messageIDs), string(labelID)) +} + +// UnlabelMessages unlabels the given messages with the given label ID. +func (conn *imapConnector) UnlabelMessages(ctx context.Context, messageIDs []imap.MessageID, labelID imap.LabelID) error { + return conn.client.UnlabelMessages(ctx, strMessageIDs(messageIDs), string(labelID)) +} + +// MoveMessages removes the given messages from one label and adds them to the other label. +func (conn *imapConnector) MoveMessages(ctx context.Context, messageIDs []imap.MessageID, labelFromID imap.LabelID, labelToID imap.LabelID) error { + if err := conn.client.LabelMessages(ctx, strMessageIDs(messageIDs), string(labelToID)); err != nil { + return fmt.Errorf("labeling messages: %w", err) + } + + if err := conn.client.UnlabelMessages(ctx, strMessageIDs(messageIDs), string(labelFromID)); err != nil { + return fmt.Errorf("unlabeling messages: %w", err) + } + + return nil +} + +// MarkMessagesSeen sets the seen value of the given messages. +func (conn *imapConnector) MarkMessagesSeen(ctx context.Context, messageIDs []imap.MessageID, seen bool) error { + if seen { + return conn.client.MarkMessagesRead(ctx, strMessageIDs(messageIDs)...) + } else { + return conn.client.MarkMessagesUnread(ctx, strMessageIDs(messageIDs)...) + } +} + +// MarkMessagesFlagged sets the flagged value of the given messages. +func (conn *imapConnector) MarkMessagesFlagged(ctx context.Context, messageIDs []imap.MessageID, flagged bool) error { + if flagged { + return conn.client.LabelMessages(ctx, strMessageIDs(messageIDs), liteapi.StarredLabel) + } else { + return conn.client.UnlabelMessages(ctx, strMessageIDs(messageIDs), liteapi.StarredLabel) + } +} + +// GetUpdates returns a stream of updates that the gluon server should apply. +// It is recommended that the returned channel is buffered with at least constants.ChannelBufferCount. +func (conn *imapConnector) GetUpdates() <-chan imap.Update { + return conn.updateCh +} + +// Close the connector when it will no longer be used and all resources should be closed/released. +func (conn *imapConnector) Close(ctx context.Context) error { + return nil +} + +func (conn *imapConnector) addAddress(address string) { + conn.addresses = append(conn.addresses, address) +} + +func (conn *imapConnector) remAddress(address string) { + idx := slices.Index(conn.addresses, address) + + if idx < 0 { + return + } + + conn.addresses = append(conn.addresses[:idx], conn.addresses[idx+1:]...) +} + +func strLabelIDs(imapLabelIDs []imap.LabelID) []string { + return xslices.Map(imapLabelIDs, func(labelID imap.LabelID) string { + return string(labelID) + }) +} + +func imapLabelIDs(labelIDs []string) []imap.LabelID { + return xslices.Map(labelIDs, func(labelID string) imap.LabelID { + return imap.LabelID(labelID) + }) +} + +func strMessageIDs(imapMessageIDs []imap.MessageID) []string { + return xslices.Map(imapMessageIDs, func(messageID imap.MessageID) string { + return string(messageID) + }) +} + +func imapMessageIDs(messageIDs []string) []imap.MessageID { + return xslices.Map(messageIDs, func(messageID string) imap.MessageID { + return imap.MessageID(messageID) + }) +} diff --git a/internal/user/smtp.go b/internal/user/smtp.go new file mode 100644 index 00000000..d50fb871 --- /dev/null +++ b/internal/user/smtp.go @@ -0,0 +1,330 @@ +package user + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "runtime" + "strings" + + "github.com/ProtonMail/gluon/rfc822" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/proton-bridge/v2/pkg/message" + "github.com/ProtonMail/proton-bridge/v2/pkg/message/parser" + "github.com/bradenaw/juniper/parallel" + "github.com/bradenaw/juniper/xslices" + "github.com/emersion/go-smtp" + "github.com/sirupsen/logrus" + "gitlab.protontech.ch/go/liteapi" +) + +type smtpSession struct { + client *liteapi.Client + username string + addresses []liteapi.Address + userKR *crypto.KeyRing + addrKRs map[string]*crypto.KeyRing + settings liteapi.MailSettings + + from string + to map[string]struct{} +} + +func newSMTPSession( + client *liteapi.Client, + username string, + addresses []liteapi.Address, + userKR *crypto.KeyRing, + addrKRs map[string]*crypto.KeyRing, + settings liteapi.MailSettings, +) *smtpSession { + return &smtpSession{ + client: client, + username: username, + addresses: addresses, + userKR: userKR, + addrKRs: addrKRs, + settings: settings, + + from: "", + to: make(map[string]struct{}), + } +} + +// Discard currently processed message. +func (session *smtpSession) Reset() { + logrus.Info("SMTP session reset") + + // Clear the from and to fields. + session.from = "" + session.to = make(map[string]struct{}) +} + +// Free all resources associated with session. +func (session *smtpSession) Logout() error { + defer session.Reset() + + logrus.Info("SMTP session logout") + + return nil +} + +// Set return path for currently processed message. +func (session *smtpSession) Mail(from string, opts smtp.MailOptions) error { + logrus.Info("SMTP session mail") + + if opts.RequireTLS { + return ErrNotImplemented + } + + if opts.UTF8 { + return ErrNotImplemented + } + + if opts.Auth != nil && *opts.Auth != "" && *opts.Auth != session.username { + return ErrNotImplemented + } + + idx := xslices.IndexFunc(session.addresses, func(address liteapi.Address) bool { + return strings.EqualFold(address.Email, from) + }) + + if idx < 0 { + return ErrInvalidReturnPath + } + + session.from = session.addresses[idx].ID + + return nil +} + +// Add recipient for currently processed message. +func (session *smtpSession) Rcpt(to string) error { + logrus.Info("SMTP session rcpt") + + if to == "" { + return ErrInvalidRecipient + } + + session.to[to] = struct{}{} + + return nil +} + +// Set currently processed message contents and send it. +func (session *smtpSession) Data(r io.Reader) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logrus.Info("SMTP session data") + + if session.from == "" { + return ErrInvalidReturnPath + } + + if len(session.to) == 0 { + return ErrInvalidRecipient + } + + addrKR, ok := session.addrKRs[session.from] + if !ok { + return ErrMissingAddressKey + } + + addrKR, err := addrKR.FirstKey() + if err != nil { + return fmt.Errorf("failed to get first key: %w", err) + } + + parser, err := parser.New(r) + if err != nil { + return fmt.Errorf("failed to create parser: %w", err) + } + + if session.settings.AttachPublicKey == liteapi.AttachPublicKeyEnabled { + key, err := addrKR.GetKey(0) + if err != nil { + return fmt.Errorf("failed to get user public key: %w", err) + } + + pubKey, err := key.GetArmoredPublicKey() + if err != nil { + return fmt.Errorf("failed to get user public key: %w", err) + } + + parser.AttachPublicKey(pubKey, fmt.Sprintf("publickey - %v - %v", addrKR.GetIdentities()[0].Name, key.GetFingerprint()[:8])) + } + + message, err := message.ParseWithParser(parser) + if err != nil { + return fmt.Errorf("failed to parse message: %w", err) + } + + draft, attKeys, err := session.createDraft(ctx, addrKR, message) + if err != nil { + return fmt.Errorf("failed to create draft: %w", err) + } + + recipients, err := session.getRecipients(ctx, message.Recipients(), message.MIMEType) + if err != nil { + return fmt.Errorf("failed to get recipients: %w", err) + } + + req, err := createSendReq(addrKR, message.MIMEBody, message.RichBody, message.PlainBody, recipients, attKeys) + if err != nil { + return fmt.Errorf("failed to create packages: %w", err) + } + + res, err := session.client.SendDraft(ctx, draft.ID, req) + if err != nil { + return fmt.Errorf("failed to send draft: %w", err) + } + + logrus.WithField("messageID", res.ID).Info("SMTP message sent") + + return nil +} + +func (session *smtpSession) createDraft(ctx context.Context, addrKR *crypto.KeyRing, message message.Message) (liteapi.Message, map[string]*crypto.SessionKey, error) { + encBody, err := addrKR.Encrypt(crypto.NewPlainMessageFromString(string(message.RichBody)), nil) + if err != nil { + return liteapi.Message{}, nil, fmt.Errorf("failed to encrypt message body: %w", err) + } + + armBody, err := encBody.GetArmored() + if err != nil { + return liteapi.Message{}, nil, fmt.Errorf("failed to armor message body: %w", err) + } + + draft, err := session.client.CreateDraft(ctx, liteapi.CreateDraftReq{ + Message: liteapi.DraftTemplate{ + Subject: message.Subject, + Sender: message.Sender, + ToList: message.ToList, + CCList: message.CCList, + BCCList: message.BCCList, + Body: armBody, + }, + AttachmentKeyPackets: []string{}, + }) + if err != nil { + return liteapi.Message{}, nil, fmt.Errorf("failed to create draft: %w", err) + } + + attKeys, err := session.createAttachments(ctx, addrKR, draft.ID, message.Attachments) + if err != nil { + return liteapi.Message{}, nil, fmt.Errorf("failed to create attachments: %w", err) + } + + return draft, attKeys, nil +} + +func (session *smtpSession) createAttachments(ctx context.Context, addrKR *crypto.KeyRing, draftID string, attachments []message.Attachment) (map[string]*crypto.SessionKey, error) { + type attKey struct { + attID string + key *crypto.SessionKey + } + + keys, err := parallel.MapContext(ctx, runtime.NumCPU(), attachments, func(ctx context.Context, att message.Attachment) (attKey, error) { + sig, err := addrKR.SignDetached(crypto.NewPlainMessage(att.Data)) + if err != nil { + return attKey{}, fmt.Errorf("failed to sign attachment: %w", err) + } + + encData, err := addrKR.EncryptAttachment(crypto.NewPlainMessage(att.Data), att.Name) + if err != nil { + return attKey{}, fmt.Errorf("failed to encrypt attachment: %w", err) + } + + attachment, err := session.client.UploadAttachment(ctx, liteapi.CreateAttachmentReq{ + Filename: att.Name, + MessageID: draftID, + MIMEType: rfc822.MIMEType(att.MIMEType), + Disposition: liteapi.Disposition(att.Disposition), + ContentID: att.ContentID, + KeyPackets: encData.KeyPacket, + DataPacket: encData.DataPacket, + Signature: sig.GetBinary(), + }) + if err != nil { + return attKey{}, fmt.Errorf("failed to upload attachment: %w", err) + } + + keyPacket, err := base64.StdEncoding.DecodeString(attachment.KeyPackets) + if err != nil { + return attKey{}, fmt.Errorf("failed to decode key packets: %w", err) + } + + key, err := addrKR.DecryptSessionKey(keyPacket) + if err != nil { + return attKey{}, fmt.Errorf("failed to decrypt session key: %w", err) + } + + return attKey{attID: attachment.ID, key: key}, nil + }) + if err != nil { + return nil, fmt.Errorf("failed to create attachments: %w", err) + } + + attKeys := make(map[string]*crypto.SessionKey) + + for _, key := range keys { + attKeys[key.attID] = key.key + } + + return attKeys, nil +} + +func (session *smtpSession) getRecipients(ctx context.Context, addresses []string, mimeType rfc822.MIMEType) (recipients, error) { + prefs, err := parallel.MapContext(ctx, runtime.NumCPU(), addresses, func(ctx context.Context, address string) (liteapi.SendPreferences, error) { + return session.getSendPrefs(ctx, address, mimeType) + }) + if err != nil { + return nil, fmt.Errorf("failed to get recipients: %w", err) + } + + recipients := make(recipients) + + for idx, pref := range prefs { + recipients[addresses[idx]] = pref + } + + return recipients, nil +} + +func (session *smtpSession) getSendPrefs(ctx context.Context, recipient string, mimeType rfc822.MIMEType) (liteapi.SendPreferences, error) { + pubKeys, internal, err := session.client.GetPublicKeys(ctx, recipient) + if err != nil { + return liteapi.SendPreferences{}, fmt.Errorf("failed to get public keys: %w", err) + } + + settings, err := session.getContactSettings(ctx, recipient) + if err != nil { + return liteapi.SendPreferences{}, fmt.Errorf("failed to get contact settings: %w", err) + } + + return buildSendPrefs(settings, session.settings, pubKeys, mimeType, internal) +} + +func (session *smtpSession) getContactSettings(ctx context.Context, recipient string) (liteapi.ContactSettings, error) { + contacts, err := session.client.GetAllContactEmails(ctx, recipient) + if err != nil { + return liteapi.ContactSettings{}, fmt.Errorf("failed to get contact data: %w", err) + } + + idx := xslices.IndexFunc(contacts, func(contact liteapi.ContactEmail) bool { + return contact.Email == recipient + }) + + if idx < 0 { + return liteapi.ContactSettings{}, nil + } + + contact, err := session.client.GetContact(ctx, contacts[idx].ContactID) + if err != nil { + return liteapi.ContactSettings{}, fmt.Errorf("failed to get contact: %w", err) + } + + return contact.GetSettings(session.userKR, recipient) +} diff --git a/internal/user/smtp_packages.go b/internal/user/smtp_packages.go new file mode 100644 index 00000000..57f8ffa4 --- /dev/null +++ b/internal/user/smtp_packages.go @@ -0,0 +1,69 @@ +package user + +import ( + "github.com/ProtonMail/gluon/rfc822" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/proton-bridge/v2/pkg/message" + "github.com/bradenaw/juniper/xslices" + "gitlab.protontech.ch/go/liteapi" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +func createSendReq( + kr *crypto.KeyRing, + mimeBody message.MIMEBody, + richBody, plainBody message.Body, + recipients recipients, + attKeys map[string]*crypto.SessionKey, +) (liteapi.SendDraftReq, error) { + var req liteapi.SendDraftReq + + if recs := recipients.scheme(liteapi.PGPMIMEScheme, liteapi.ClearMIMEScheme); len(recs) > 0 { + if err := req.AddMIMEPackage(kr, string(mimeBody), recs); err != nil { + return liteapi.SendDraftReq{}, err + } + } + + if recs := recipients.scheme(liteapi.InternalScheme, liteapi.ClearScheme, liteapi.PGPInlineScheme); len(recs) > 0 { + if recs := recs.content(rfc822.TextHTML); len(recs) > 0 { + if err := req.AddPackage(kr, string(richBody), rfc822.TextHTML, recs, attKeys); err != nil { + return liteapi.SendDraftReq{}, err + } + } + + if recs := recs.content(rfc822.TextPlain); len(recs) > 0 { + if err := req.AddPackage(kr, string(plainBody), rfc822.TextPlain, recs, attKeys); err != nil { + return liteapi.SendDraftReq{}, err + } + } + } + + return req, nil +} + +type recipients map[string]liteapi.SendPreferences + +func (r recipients) scheme(scheme ...liteapi.EncryptionScheme) recipients { + res := make(recipients) + + for _, addr := range xslices.Filter(maps.Keys(r), func(addr string) bool { + return slices.Contains(scheme, r[addr].EncryptionScheme) + }) { + res[addr] = r[addr] + } + + return res +} + +func (r recipients) content(mimeType ...rfc822.MIMEType) recipients { + res := make(recipients) + + for _, addr := range xslices.Filter(maps.Keys(r), func(addr string) bool { + return slices.Contains(mimeType, r[addr].MIMEType) + }) { + res[addr] = r[addr] + } + + return res +} diff --git a/internal/smtp/preferences.go b/internal/user/smtp_prefs.go similarity index 74% rename from internal/smtp/preferences.go rename to internal/user/smtp_prefs.go index 8a58a2d1..835eb026 100644 --- a/internal/smtp/preferences.go +++ b/internal/user/smtp_prefs.go @@ -15,12 +15,16 @@ // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . -package smtp +package user import ( + "encoding/base64" + "fmt" + + "github.com/ProtonMail/gluon/rfc822" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" "github.com/pkg/errors" + "gitlab.protontech.ch/go/liteapi" ) const ( @@ -29,65 +33,106 @@ const ( pmInternal = "internal" // A mix between pgpInline and pgpMime used by PM. ) -// SendPreferences contains information about how to handle a message. -// It is derived from contact data, api key data, mail settings and composer preferences. -type SendPreferences struct { - // Encrypt indicates whether the email should be encrypted or not. - // If it's encrypted, we need to know which public key to use. - Encrypt bool - - // Sign indicates whether the email should be signed or not. - Sign bool - - // Scheme indicates if we should encrypt body and attachments separately and - // what MIME format to give the final encrypted email. The two standard PGP - // schemes are PGP/MIME and PGP/Inline. However we use a custom scheme for - // internal emails (including the so-called encrypted-to-outside emails, - // which even though meant for external users, they don't really get out of - // our platform). If the email is sent unencrypted, no PGP scheme is needed. - Scheme pmapi.PackageFlag - - // MIMEType is the MIME type to use for formatting the body of the email - // (before encryption/after decryption). The standard possibilities are the - // enriched HTML format, text/html, and plain text, text/plain. But it's - // also possible to have a multipart/mixed format, which is typically used - // for PGP/MIME encrypted emails, where attachments go into the body too. - // Because of this, this option is sometimes called MIME format. - MIMEType string - - // PublicKey contains an OpenPGP key that can be used for encryption. - PublicKey *crypto.KeyRing +type contactSettings struct { + Email string + Keys []string + Scheme string + Sign bool + SignIsSet bool + Encrypt bool + MIMEType rfc822.MIMEType } -type sendPreferencesBuilder struct { - internal bool - encrypt *bool - sign *bool - scheme *string - mimeType *string +// newContactSettings converts the API settings into our local settings. +// This is due to the legacy send preferences code. +func newContactSettings(settings liteapi.ContactSettings) *contactSettings { + metadata := &contactSettings{} + if settings.MIMEType != nil { + metadata.MIMEType = *settings.MIMEType + } + + if settings.Sign != nil { + metadata.Sign = *settings.Sign + metadata.SignIsSet = true + } + + if settings.Encrypt != nil { + metadata.Encrypt = *settings.Encrypt + } + + if settings.Scheme != nil { + switch *settings.Scheme { + case liteapi.PGPMIMEScheme: + metadata.Scheme = pgpMIME + + case liteapi.PGPInlineScheme: + metadata.Scheme = pgpInline + } + } + + if settings.Keys != nil { + for _, key := range settings.Keys { + b, err := key.Serialize() + if err != nil { + panic(err) + } + + metadata.Keys = append(metadata.Keys, base64.StdEncoding.EncodeToString(b)) + } + } + + return metadata +} + +func buildSendPrefs( + contactSettings liteapi.ContactSettings, + mailSettings liteapi.MailSettings, + pubKeys []liteapi.PublicKey, + mimeType rfc822.MIMEType, + isInternal bool, +) (liteapi.SendPreferences, error) { + builder := &sendPrefsBuilder{} + + if err := builder.setPGPSettings(newContactSettings(contactSettings), pubKeys, isInternal); err != nil { + return liteapi.SendPreferences{}, fmt.Errorf("failed to set PGP settings: %w", err) + } + + builder.setEncryptionPreferences(mailSettings) + + builder.setMIMEPreferences(string(mimeType)) + + return builder.build(), nil +} + +type sendPrefsBuilder struct { + internal bool + encrypt *bool + sign *bool + scheme *string + mimeType *rfc822.MIMEType publicKey *crypto.KeyRing } -func (b *sendPreferencesBuilder) withInternal() { +func (b *sendPrefsBuilder) withInternal() { b.internal = true } -func (b *sendPreferencesBuilder) isInternal() bool { +func (b *sendPrefsBuilder) isInternal() bool { return b.internal } -func (b *sendPreferencesBuilder) withEncrypt(v bool) { +func (b *sendPrefsBuilder) withEncrypt(v bool) { b.encrypt = &v } -func (b *sendPreferencesBuilder) withEncryptDefault(v bool) { +func (b *sendPrefsBuilder) withEncryptDefault(v bool) { if b.encrypt == nil { b.encrypt = &v } } -func (b *sendPreferencesBuilder) shouldEncrypt() bool { +func (b *sendPrefsBuilder) shouldEncrypt() bool { if b.encrypt != nil { return *b.encrypt } @@ -95,18 +140,18 @@ func (b *sendPreferencesBuilder) shouldEncrypt() bool { return false } -func (b *sendPreferencesBuilder) withSign(sign bool) { +func (b *sendPrefsBuilder) withSign(sign bool) { b.sign = &sign } -func (b *sendPreferencesBuilder) withSignDefault() { +func (b *sendPrefsBuilder) withSignDefault() { v := true if b.sign == nil { b.sign = &v } } -func (b *sendPreferencesBuilder) shouldSign() bool { +func (b *sendPrefsBuilder) shouldSign() bool { if b.sign != nil { return *b.sign } @@ -114,17 +159,17 @@ func (b *sendPreferencesBuilder) shouldSign() bool { return false } -func (b *sendPreferencesBuilder) withScheme(v string) { +func (b *sendPrefsBuilder) withScheme(v string) { b.scheme = &v } -func (b *sendPreferencesBuilder) withSchemeDefault(v string) { +func (b *sendPrefsBuilder) withSchemeDefault(v string) { if b.scheme == nil { b.scheme = &v } } -func (b *sendPreferencesBuilder) getScheme() string { +func (b *sendPrefsBuilder) getScheme() string { if b.scheme != nil { return *b.scheme } @@ -132,21 +177,21 @@ func (b *sendPreferencesBuilder) getScheme() string { return "" } -func (b *sendPreferencesBuilder) withMIMEType(v string) { +func (b *sendPrefsBuilder) withMIMEType(v rfc822.MIMEType) { b.mimeType = &v } -func (b *sendPreferencesBuilder) withMIMETypeDefault(v string) { +func (b *sendPrefsBuilder) withMIMETypeDefault(v rfc822.MIMEType) { if b.mimeType == nil { b.mimeType = &v } } -func (b *sendPreferencesBuilder) removeMIMEType() { +func (b *sendPrefsBuilder) removeMIMEType() { b.mimeType = nil } -func (b *sendPreferencesBuilder) getMIMEType() string { +func (b *sendPrefsBuilder) getMIMEType() rfc822.MIMEType { if b.mimeType != nil { return *b.mimeType } @@ -154,7 +199,7 @@ func (b *sendPreferencesBuilder) getMIMEType() string { return "" } -func (b *sendPreferencesBuilder) withPublicKey(v *crypto.KeyRing) { +func (b *sendPrefsBuilder) withPublicKey(v *crypto.KeyRing) { b.publicKey = v } @@ -175,32 +220,37 @@ func (b *sendPreferencesBuilder) withPublicKey(v *crypto.KeyRing) { // mimeType: 'text/html' | 'text/plain' | 'multipart/mixed', // publicKey: OpenPGPKey | undefined/null // }. -func (b *sendPreferencesBuilder) build() (p SendPreferences) { +func (b *sendPrefsBuilder) build() (p liteapi.SendPreferences) { p.Encrypt = b.shouldEncrypt() - p.Sign = b.shouldSign() p.MIMEType = b.getMIMEType() - p.PublicKey = b.publicKey + p.PubKey = b.publicKey + + if b.shouldSign() { + p.SignatureType = liteapi.DetachedSignature + } else { + p.SignatureType = liteapi.NoSignature + } switch { case b.isInternal(): - p.Scheme = pmapi.InternalPackage + p.EncryptionScheme = liteapi.InternalScheme case b.shouldSign() && b.shouldEncrypt(): if b.getScheme() == pgpInline { - p.Scheme = pmapi.PGPInlinePackage + p.EncryptionScheme = liteapi.PGPInlineScheme } else { - p.Scheme = pmapi.PGPMIMEPackage + p.EncryptionScheme = liteapi.PGPMIMEScheme } case b.shouldSign() && !b.shouldEncrypt(): if b.getScheme() == pgpInline { - p.Scheme = pmapi.ClearPackage + p.EncryptionScheme = liteapi.ClearScheme } else { - p.Scheme = pmapi.ClearMIMEPackage + p.EncryptionScheme = liteapi.ClearMIMEScheme } default: - p.Scheme = pmapi.ClearPackage + p.EncryptionScheme = liteapi.ClearScheme } return @@ -218,14 +268,14 @@ func (b *sendPreferencesBuilder) build() (p SendPreferences) { // // These settings are simply a reflection of the vCard content plus the public // key info retrieved from the API via the GET KEYS route. -func (b *sendPreferencesBuilder) setPGPSettings( - vCardData *ContactMetadata, - apiKeys []pmapi.PublicKey, +func (b *sendPrefsBuilder) setPGPSettings( + vCardData *contactSettings, + apiKeys []liteapi.PublicKey, isInternal bool, ) (err error) { // If there is no contact metadata, we can just use a default constructed one. if vCardData == nil { - vCardData = &ContactMetadata{} + vCardData = &contactSettings{} } // Sending internal. @@ -250,9 +300,9 @@ func (b *sendPreferencesBuilder) setPGPSettings( // An internal address can be either an obvious one: abc@protonmail.com, // abc@protonmail.ch or abc@pm.me, or one belonging to a custom domain // registered with proton. -func (b *sendPreferencesBuilder) setInternalPGPSettings( - vCardData *ContactMetadata, - apiKeys []pmapi.PublicKey, +func (b *sendPrefsBuilder) setInternalPGPSettings( + vCardData *contactSettings, + apiKeys []liteapi.PublicKey, ) (err error) { // We're guaranteed to get at least one valid (i.e. not expired, revoked or // marked as verification-only) public key from the server. @@ -297,7 +347,7 @@ func (b *sendPreferencesBuilder) setInternalPGPSettings( // 3. If there are no pinned keys, then the client should encrypt with the // first valid key served by the API (in principle the server already // validates the keys and the first one provided should be valid). -func pickSendingKey(vCardData *ContactMetadata, rawAPIKeys []pmapi.PublicKey) (kr *crypto.KeyRing, err error) { +func pickSendingKey(vCardData *contactSettings, rawAPIKeys []liteapi.PublicKey) (kr *crypto.KeyRing, err error) { contactKeys := make([]*crypto.Key, len(vCardData.Keys)) apiKeys := make([]*crypto.Key, len(rawAPIKeys)) @@ -361,9 +411,9 @@ func matchFingerprints(a, b []*crypto.Key) (res []*crypto.Key) { return } -func (b *sendPreferencesBuilder) setExternalPGPSettingsWithWKDKeys( - vCardData *ContactMetadata, - apiKeys []pmapi.PublicKey, +func (b *sendPrefsBuilder) setExternalPGPSettingsWithWKDKeys( + vCardData *contactSettings, + apiKeys []liteapi.PublicKey, ) (err error) { // We're guaranteed to get at least one valid (i.e. not expired, revoked or // marked as verification-only) public key from the server. @@ -401,8 +451,8 @@ func (b *sendPreferencesBuilder) setExternalPGPSettingsWithWKDKeys( return nil } -func (b *sendPreferencesBuilder) setExternalPGPSettingsWithoutWKDKeys( - vCardData *ContactMetadata, +func (b *sendPrefsBuilder) setExternalPGPSettingsWithoutWKDKeys( + vCardData *contactSettings, ) (err error) { b.withEncrypt(vCardData.Encrypt) @@ -468,7 +518,7 @@ func (b *sendPreferencesBuilder) setExternalPGPSettingsWithoutWKDKeys( // // The public key can still be undefined as we do not need it if the outgoing // email is not encrypted. -func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.MailSettings) { +func (b *sendPrefsBuilder) setEncryptionPreferences(mailSettings liteapi.MailSettings) { // For internal addresses or external ones with WKD keys, this flag should // always be true. For external ones, an undefined flag defaults to false. b.withEncryptDefault(false) @@ -489,11 +539,11 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai // If undefined, default to the user mail setting "Default PGP scheme". // Otherwise keep the defined value. switch mailSettings.PGPScheme { - case pmapi.PGPInlinePackage: + case liteapi.PGPInlineScheme: b.withSchemeDefault(pgpInline) - case pmapi.PGPMIMEPackage: + case liteapi.PGPMIMEScheme: b.withSchemeDefault(pgpMIME) - case pmapi.ClearMIMEPackage, pmapi.ClearPackage, pmapi.EncryptedOutsidePackage, pmapi.InternalPackage: + case liteapi.ClearMIMEScheme, liteapi.ClearScheme, liteapi.EncryptedOutsideScheme, liteapi.InternalScheme: // nothing to set } @@ -509,7 +559,7 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai } } -func (b *sendPreferencesBuilder) setMIMEPreferences(composerMIMEType string) { +func (b *sendPrefsBuilder) setMIMEPreferences(composerMIMEType string) { // If the sign flag (that we just determined above) is true, then the MIME // type is determined by the PGP scheme (also determined above): we should // use 'text/plain' for a PGP/Inline scheme, and 'multipart/mixed' otherwise. diff --git a/internal/user/smtp_prefs_test.go b/internal/user/smtp_prefs_test.go new file mode 100644 index 00000000..060d1810 --- /dev/null +++ b/internal/user/smtp_prefs_test.go @@ -0,0 +1,445 @@ +// Copyright (c) 2022 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package user + +import ( + "testing" + + "github.com/ProtonMail/gluon/rfc822" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.protontech.ch/go/liteapi" +) + +func TestPreferencesBuilder(t *testing.T) { + testContactKey := loadContactKey(t, testPublicKey) + testOtherContactKey := loadContactKey(t, testOtherPublicKey) + + tests := []struct { //nolint:maligned + name string + + contactMeta *contactSettings + receivedKeys []liteapi.PublicKey + isInternal bool + mailSettings liteapi.MailSettings + composerMIMEType string + + wantEncrypt bool + wantSign liteapi.SignatureType + wantScheme liteapi.EncryptionScheme + wantMIMEType rfc822.MIMEType + wantPublicKey string + }{ + { + name: "internal", + + contactMeta: &contactSettings{}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: true, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.InternalScheme, + wantMIMEType: "text/html", + wantPublicKey: testPublicKey, + }, + + { + name: "internal with contact-specific email format", + + contactMeta: &contactSettings{MIMEType: "text/plain"}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: true, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.InternalScheme, + wantMIMEType: "text/plain", + wantPublicKey: testPublicKey, + }, + + { + name: "internal with pinned contact public key", + + contactMeta: &contactSettings{Keys: []string{testContactKey}}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: true, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.InternalScheme, + wantMIMEType: "text/html", + wantPublicKey: testPublicKey, + }, + + { + // NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation. + name: "internal with conflicting contact public key", + + contactMeta: &contactSettings{Keys: []string{testOtherContactKey}}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: true, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.InternalScheme, + wantMIMEType: "text/html", + wantPublicKey: testPublicKey, + }, + + { + name: "wkd-external", + + contactMeta: &contactSettings{}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPMIMEScheme, + wantMIMEType: "multipart/mixed", + wantPublicKey: testPublicKey, + }, + + { + name: "wkd-external with contact-specific email format", + + contactMeta: &contactSettings{MIMEType: "text/plain"}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPMIMEScheme, + wantMIMEType: "multipart/mixed", + wantPublicKey: testPublicKey, + }, + + { + name: "wkd-external with global pgp-inline scheme", + + contactMeta: &contactSettings{}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPInlineScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPInlineScheme, + wantMIMEType: "text/plain", + wantPublicKey: testPublicKey, + }, + + { + name: "wkd-external with contact-specific pgp-inline scheme overriding global pgp-mime setting", + + contactMeta: &contactSettings{Scheme: pgpInline}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPInlineScheme, + wantMIMEType: "text/plain", + wantPublicKey: testPublicKey, + }, + + { + name: "wkd-external with contact-specific pgp-mime scheme overriding global pgp-inline setting", + + contactMeta: &contactSettings{Scheme: pgpMIME}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPInlineScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPMIMEScheme, + wantMIMEType: "multipart/mixed", + wantPublicKey: testPublicKey, + }, + + { + name: "wkd-external with additional pinned contact public key", + + contactMeta: &contactSettings{Keys: []string{testContactKey}}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPMIMEScheme, + wantMIMEType: "multipart/mixed", + wantPublicKey: testPublicKey, + }, + + { + // NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation. + name: "wkd-external with additional conflicting contact public key", + + contactMeta: &contactSettings{Keys: []string{testOtherContactKey}}, + receivedKeys: []liteapi.PublicKey{{PublicKey: testPublicKey}}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPMIMEScheme, + wantMIMEType: "multipart/mixed", + wantPublicKey: testPublicKey, + }, + + { + name: "external", + + contactMeta: &contactSettings{}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: false, + wantSign: liteapi.NoSignature, + wantScheme: liteapi.ClearScheme, + wantMIMEType: "text/html", + }, + + { + name: "external with contact-specific email format", + + contactMeta: &contactSettings{MIMEType: "text/plain"}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: false, + wantSign: liteapi.NoSignature, + wantScheme: liteapi.ClearScheme, + wantMIMEType: "text/plain", + }, + + { + name: "external with sign enabled", + + contactMeta: &contactSettings{Sign: true, SignIsSet: true}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: false, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.ClearMIMEScheme, + wantMIMEType: "multipart/mixed", + }, + + { + name: "external with contact sign enabled and plain text", + + contactMeta: &contactSettings{MIMEType: "text/plain", Scheme: pgpInline, Sign: true, SignIsSet: true}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: false, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.ClearScheme, + wantMIMEType: "text/plain", + }, + + { + name: "external with sign enabled, sending plaintext, should still send as ClearMIME", + + contactMeta: &contactSettings{Sign: true, SignIsSet: true}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/plain"}, + + wantEncrypt: false, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.ClearMIMEScheme, + wantMIMEType: "multipart/mixed", + }, + + { + name: "external with pinned contact public key but no intention to encrypt/sign", + + contactMeta: &contactSettings{Keys: []string{testContactKey}}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: false, + wantSign: liteapi.NoSignature, + wantScheme: liteapi.ClearScheme, + wantMIMEType: "text/html", + wantPublicKey: testPublicKey, + }, + + { + name: "external with pinned contact public key, encrypted and signed", + + contactMeta: &contactSettings{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPMIMEScheme, + wantMIMEType: "multipart/mixed", + wantPublicKey: testPublicKey, + }, + + { + name: "external with pinned contact public key, encrypted and signed using contact-specific pgp-inline", + + contactMeta: &contactSettings{Keys: []string{testContactKey}, Encrypt: true, Sign: true, Scheme: pgpInline, SignIsSet: true}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPMIMEScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPInlineScheme, + wantMIMEType: "text/plain", + wantPublicKey: testPublicKey, + }, + + { + name: "external with pinned contact public key, encrypted and signed using global pgp-inline", + + contactMeta: &contactSettings{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true}, + receivedKeys: []liteapi.PublicKey{}, + isInternal: false, + mailSettings: liteapi.MailSettings{PGPScheme: liteapi.PGPInlineScheme, DraftMIMEType: "text/html"}, + + wantEncrypt: true, + wantSign: liteapi.DetachedSignature, + wantScheme: liteapi.PGPInlineScheme, + wantMIMEType: "text/plain", + wantPublicKey: testPublicKey, + }, + } + + for _, test := range tests { + test := test // Avoid using range scope test inside function literal. + + t.Run(test.name, func(t *testing.T) { + b := &sendPrefsBuilder{} + + require.NoError(t, b.setPGPSettings(test.contactMeta, test.receivedKeys, test.isInternal)) + b.setEncryptionPreferences(test.mailSettings) + b.setMIMEPreferences(test.composerMIMEType) + + prefs := b.build() + + assert.Equal(t, test.wantEncrypt, prefs.Encrypt) + assert.Equal(t, test.wantSign, prefs.SignatureType) + assert.Equal(t, test.wantScheme, prefs.EncryptionScheme) + assert.Equal(t, test.wantMIMEType, prefs.MIMEType) + + if prefs.PubKey != nil { + wantKey, err := crypto.NewKeyFromArmored(test.wantPublicKey) + require.NoError(t, err) + + haveKey, err := prefs.PubKey.GetKey(0) + require.NoError(t, err) + + assert.Equal(t, wantKey.GetFingerprint(), haveKey.GetFingerprint()) + } + }) + } +} + +func loadContactKey(t *testing.T, key string) string { + ck, err := crypto.NewKeyFromArmored(key) + require.NoError(t, err) + + pk, err := ck.GetPublicKey() + require.NoError(t, err) + + return string(pk) +} + +const testPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsBNBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefEWSHl +CjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39vPiLJXUq +Zs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKiMeVa+GLEHhgZ +2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5c8CmpqJuASIJNrSX +M/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrbDEVRA2/BCJonw7aASiNC +rSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEBAAHNBlVzZXJJRMLAcgQQAQgA +JgUCVEltzwYLCQgHAwIJED62JZ7fId8kBBUIAgoDFgIBAhsDAh4BAAD0nQf9EtH9 +TC0JqSs8q194Zo244jjlJFM3EzxOSULq0zbywlLORfyoo/O8jU/HIuGz+LT98JDt +nltTqfjWgu6pS3ZL2/L4AGUKEoB7OI6oIdRwzMc61sqI+Qpbzxo7rzufH4CiXZc6 +cxORUgL550xSCcqnq0q1mds7h5roKDzxMW6WLiEsc1dN8IQKzC7Ec5wA7U4oNGsJ +3TyI8jkIs0IhXrRCd26K0TW8Xp6GCsfblWXosR13y89WVNgC+xrrJKTZEisc0tRl +neIgjcwEUvwfIg2n9cDUFA/5BsfzTW5IurxqDEziIVP0L44PXjtJrBQaGMPlEbtP +5i2oi3OADVX2XbvsRc7ATQRUSW3PAQgAkPnu5fps5zhOB/e618v/iF3KiogxUeRh +A68TbvA+xnFfTxCx2Vo14aOL0CnaJ8gO5yRSqfomL2O1kMq07N1MGbqucbmc+aSf +oElc+Gd5xBE/w3RcEhKcAaYTi35vG22zlZup4x3ElioyIarOssFEkQgNNyDf5AXZ +jdHLA6qVxeqAb/Ff74+y9HUmLPSsRU9NwFzvK3Jv8C/ubHVLzTYdFgYkc4W1Uug9 +Ou08K+/4NEMrwnPFBbZdJAuUjQz2zW2ZiEKiBggiorH2o5N3mYUnWEmUvqL3EOS8 +TbWo8UBIW3DDm2JiZR8VrEgvBtc9mVDUj/x+5pR07Fy1D6DjRmAc9wARAQABwsBf +BBgBCAATBQJUSW3SCRA+tiWe3yHfJAIbDAAA/iwH/ik9RKZMB9Ir0x5mGpKPuqhu +gwrc3d04m1sOdXJm2NtD4ddzSEvzHwaPNvEvUl5v7FVMzf6+6mYGWHyNP4+e7Rtw +YLlRpud6smuGyDSsotUYyumiqP6680ZIeWVQ+a1TThNs878mAJy1FhvQFdTmA8XI +C616hDFpamQKPlpoO1a0wZnQhrPwT77HDYEEa+hqY4Jr/a7ui40S+7xYRHKL/7ZA +S4/grWllhU3dbNrwSzrOKwrA/U0/9t738Ap6JL71YymDeaL4sutcoaahda1pTrMW +ePtrCltz6uySwbZs7GXoEzjX3EAH+6qhkUJtzMaE3YEFEoQMGzcDTUEfXCJ3zJw= +=yT9U +-----END PGP PUBLIC KEY BLOCK-----` + +const testOtherPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF8Rmj4BCACgXXxRqLsmEUWZGd0f88BteXBfi9zL+9GysOTk4n9EgINLN2PU +5rYSmWvVocO8IAfl/z9zpTJQesQjGe5lHbygUWFmjadox2ZeecZw0PWCSRdAjk6w +Q4UX0JiCo3IuICZk1t53WWRtGnhA2Q21J4b2DJg4T5ZFKgKDzDhWoGF1ZStbI5X1 +0rKTGFNHgreV5PqxUjxHVtx3rgT9Mx+13QTffqKR9oaYC6mNs4TNJdhyqfaYxqGw +ElxfdS9Wz6ODXrUNuSHETfgvAmo1Qep7GkefrC1isrmXA2+a+mXzFn4L0FCG073w +Vi/lEw6R/vKfN6QukHPxwoSguow4wTyhRRmfABEBAAG0GVRlc3RUZXN0IDx0ZXN0 +dGVzdEBwbS5tZT6JAU4EEwEIADgWIQTsXZU1AxlWCPT02+BKdWAu4Q1jXQUCXxGa +PgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBKdWAu4Q1jXQw+B/0ZudN+ +W9EqJtL/elm7Qla47zNsFmB+pHObdGoKtp3mNc97CQoW1yQ/i/V0heBFTAioP00g +FgEk1ZUJfO++EtI8esNFdDZqY99826/Cl0FlJwubn/XYxi4XyaGTY1nhhyEJ2HWI +/mZ+Jfm9ojbHSLwO5/AHiQt5t+LPDsKLXZw1BDJTgf1xD6e36CwAZgrPGWDqCXJ9 +BjlQn5hje7p0F8vYWBnnfSPkMHwibz9FlFqDh5v3XTgGpFIWDVkPVgAs8erM9AM2 +TjdpGcdW8xfcymo3j/o2QUBGYGJwPTsGEO5IkFRre9c/3REa7MKIi17Y479ub0A6 +2J3xgnqgI4sxmgmOuQENBF8Rmj4BCADX3BamNZsjC3I0knVIwjbz//1r8WOfNwGh +gg5LsvpfLkrsNUZy+deSwb+hS9Auyr1xsMmtVyiTPGUXTjU4uUzY2zyTYWgYfSEi +CojlXmYYLsjyPzR7KhVP6QIYZqYkOQXaCQDRlprRoFIEe4FzTCuqDHatJNwSesGy +5pPJrjiAeb47m9KaoEIacoe9D3w1z4FCKN3A8cjiWT8NRfhYTBoE/T34oXVUj8l+ +jLIgVUQgGoBos160Z1Cnxd2PKWFVh/Br3QtIPTbNVDWhh5T1+N2ypbwsXCawy6fj +cbOaTLz/vF9g+RJKC0MtxdL5qUtv3d3Zn07Sg+9H6wjsboAdAvirABEBAAGJATYE +GAEIACAWIQTsXZU1AxlWCPT02+BKdWAu4Q1jXQUCXxGaPgIbDAAKCRBKdWAu4Q1j +Xc4WB/9+aTGMMTlIdAFs9rf0i7i83pUOOxuLl34YQ0t5WGsjteQ4IK+gfuFvp37W +ktv98ShOxAexbfqzGyGcYLLgaCxCbbB85fvSeX0xK/C2UbiH3Gv1z8GTelailCxt +vyx642TwpcLXW1obHaHTSIi5L35Tce9gbug9sKCRSlAH76dANYBbMLa2Bl0LSrF8 +mcie9jJaPRXGOeHOyZmPZwwGhVYgadjptWqXnFz3ua8vxgqG0sefWF23F36iVz2q +UjxSE+nKLaPFLlEDLgxG4SwHkcR9fi7zaQVnXg4rEjr0uz5MSUqZC4MNB4rkhU3g +/rUMQyZupw+xJ+ayQNVBEtYZd/9u +=TNX4 +-----END PGP PUBLIC KEY BLOCK-----` diff --git a/internal/user/sync.go b/internal/user/sync.go new file mode 100644 index 00000000..9129168c --- /dev/null +++ b/internal/user/sync.go @@ -0,0 +1,254 @@ +package user + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/ProtonMail/gluon/imap" + "github.com/ProtonMail/proton-bridge/v2/internal/events" + "github.com/bradenaw/juniper/xslices" + "github.com/google/uuid" + "gitlab.protontech.ch/go/liteapi" + "golang.org/x/exp/slices" +) + +const chunkSize = 1 << 20 + +func (user *User) sync(ctx context.Context) error { + user.notifyCh <- events.SyncStarted{ + UserID: user.ID(), + } + + if err := user.syncLabels(ctx); 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.notifyCh <- events.SyncFinished{ + UserID: user.ID(), + } + + if err := user.vault.UpdateSync(true); err != nil { + return fmt.Errorf("failed to update sync status: %w", err) + } + + return nil +} + +func (user *User) syncLabels(ctx context.Context) error { + // Sync the system folders. + system, err := user.client.GetLabels(ctx, liteapi.LabelTypeSystem) + if err != nil { + return err + } + + for _, label := range system { + user.updateCh <- 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} { + user.updateCh <- newPlaceHolderMailboxCreatedUpdate(prefix) + } + + // Sync the API folders. + folders, err := user.client.GetLabels(ctx, liteapi.LabelTypeFolder) + if err != nil { + return err + } + + for _, folder := range folders { + user.updateCh <- newMailboxCreatedUpdate(imap.LabelID(folder.ID), []string{folderPrefix, folder.Path}) + } + + // Sync the API labels. + labels, err := user.client.GetLabels(ctx, liteapi.LabelTypeLabel) + if err != nil { + return err + } + + for _, label := range labels { + user.updateCh <- newMailboxCreatedUpdate(imap.LabelID(label.ID), []string{labelPrefix, label.Path}) + } + + return nil +} + +func (user *User) syncMessages(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + metadata, err := user.client.GetAllMessageMetadata(ctx) + if err != nil { + return err + } + + requests := xslices.Map(metadata, func(metadata liteapi.MessageMetadata) request { + return request{ + messageID: metadata.ID, + addrKR: user.addrKRs[metadata.AddressID], + } + }) + + flusher := newFlusher(user.ID(), user.updateCh, user.notifyCh, len(metadata), chunkSize) + defer flusher.flush() + + 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) + } + + flusher.push(res) + + return nil + }); err != nil { + return fmt.Errorf("failed to build messages: %w", err) + } + + return nil +} + +type flusher struct { + userID string + + updates []*imap.MessageCreated + updateCh chan<- imap.Update + notifyCh chan<- events.Event + maxChunkSize int + curChunkSize int + + count int + total int + start time.Time + + pushLock sync.Mutex +} + +func newFlusher(userID string, updateCh chan<- imap.Update, notifyCh chan<- events.Event, total, maxChunkSize int) *flusher { + return &flusher{ + userID: userID, + updateCh: updateCh, + notifyCh: notifyCh, + 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 <- imap.NewMessagesCreated(f.updates...) + f.notifyCh <- 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), + } +} + +func getMessageCreatedUpdate(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.Bool() { + 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: imapLabelIDs(filterLabelIDs(message.LabelIDs)), + ParsedMessage: parsedMessage, + }, nil +} + +func newSystemMailboxCreatedUpdate(labelID imap.LabelID, labelName string) *imap.MailboxCreated { + if strings.EqualFold(labelName, imap.Inbox) { + labelName = imap.Inbox + } + + return imap.NewMailboxCreated(imap.Mailbox{ + ID: labelID, + Name: []string{labelName}, + Flags: defaultFlags, + PermanentFlags: defaultPermanentFlags, + Attributes: imap.NewFlagSet(imap.AttrNoInferiors), + }) +} + +func newPlaceHolderMailboxCreatedUpdate(labelName string) *imap.MailboxCreated { + return imap.NewMailboxCreated(imap.Mailbox{ + ID: imap.LabelID(uuid.NewString()), + Name: []string{labelName}, + Flags: defaultFlags, + PermanentFlags: defaultPermanentFlags, + Attributes: imap.NewFlagSet(imap.AttrNoSelect), + }) +} + +func newMailboxCreatedUpdate(labelID imap.LabelID, labelName []string) *imap.MailboxCreated { + return imap.NewMailboxCreated(imap.Mailbox{ + ID: labelID, + Name: labelName, + Flags: defaultFlags, + PermanentFlags: defaultPermanentFlags, + Attributes: imap.NewFlagSet(), + }) +} + +func filterLabelIDs(labelIDs []string) []string { + var filteredLabelIDs []string + + for _, labelID := range labelIDs { + switch labelID { + case liteapi.AllDraftsLabel, liteapi.AllSentLabel, liteapi.OutboxLabel: + // ... skip ... + + default: + filteredLabelIDs = append(filteredLabelIDs, labelID) + } + } + + return filteredLabelIDs +} diff --git a/internal/user/user.go b/internal/user/user.go new file mode 100644 index 00000000..6d6a0807 --- /dev/null +++ b/internal/user/user.go @@ -0,0 +1,219 @@ +package user + +import ( + "context" + "runtime" + "time" + + "github.com/ProtonMail/gluon/connector" + "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/pool" + "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/slices" +) + +var ( + DefaultEventPeriod = 20 * time.Second + DefaultEventJitter = 20 * time.Second +) + +// TODO: Is it bad to store the key pass in the user? Any worse than storing private keys? +type User struct { + vault *vault.User + client *liteapi.Client + builder *pool.Pool[request, *imap.MessageCreated] + + apiUser liteapi.User + addresses []liteapi.Address + settings liteapi.MailSettings + + notifyCh chan events.Event + updateCh chan imap.Update + + userKR *crypto.KeyRing + addrKRs map[string]*crypto.KeyRing + imapConn *imapConnector +} + +func New( + ctx context.Context, + vault *vault.User, + client *liteapi.Client, + apiUser liteapi.User, + apiAddrs []liteapi.Address, + userKR *crypto.KeyRing, + addrKRs map[string]*crypto.KeyRing, +) (*User, error) { + if vault.EventID() == "" { + eventID, err := client.GetLatestEventID(ctx) + if err != nil { + return nil, err + } + + if err := vault.UpdateEventID(eventID); err != nil { + return nil, err + } + } + + settings, err := client.GetMailSettings(ctx) + if err != nil { + return nil, err + } + + user := &User{ + apiUser: apiUser, + addresses: apiAddrs, + settings: settings, + + vault: vault, + client: client, + builder: newBuilder(client, runtime.NumCPU()*runtime.NumCPU(), runtime.NumCPU()*runtime.NumCPU()), + + notifyCh: make(chan events.Event), + updateCh: make(chan imap.Update), + + userKR: userKR, + addrKRs: addrKRs, + } + + // When we receive an auth object, we update it in the store. + // This will be used to authorize the user on the next run. + client.AddAuthHandler(func(auth liteapi.Auth) { + if err := user.vault.UpdateAuth(auth.UID, auth.RefreshToken); err != nil { + logrus.WithError(err).Error("Failed to update auth") + } + }) + + // When we are deauthorized, we send a deauth event to the notify channel. + // Bridge will catch this and log the user out. + client.AddDeauthHandler(func() { + user.notifyCh <- events.UserDeauth{ + UserID: user.ID(), + } + }) + + // When we receive an API event, we attempt to handle it. If successful, we send the event to the event channel. + go func() { + for event := range user.client.NewEventStreamer(DefaultEventPeriod, DefaultEventJitter, vault.EventID()).Subscribe() { + if err := user.handleAPIEvent(event); err != nil { + logrus.WithError(err).Error("Failed to handle event") + } else { + if err := user.vault.UpdateEventID(event.EventID); err != nil { + logrus.WithError(err).Error("Failed to update event ID") + } + } + } + }() + + // TODO: Use a proper sync manager! (if partial sync, pickup from where we last stopped) + if !vault.HasSync() { + go user.sync(context.Background()) + } + + return user, nil +} + +func (user *User) ID() string { + return user.apiUser.ID +} + +func (user *User) Name() string { + return user.apiUser.Name +} + +func (user *User) Match(query string) bool { + if query == user.Name() { + return true + } + + return slices.Contains(user.Addresses(), query) +} + +func (user *User) Addresses() []string { + return xslices.Map( + sort(user.addresses, func(a, b liteapi.Address) bool { + return a.Order < b.Order + }), + func(address liteapi.Address) string { + return address.Email + }, + ) +} + +func (user *User) GluonID() string { + return user.vault.GluonID() +} + +func (user *User) GluonKey() []byte { + return user.vault.GluonKey() +} + +func (user *User) BridgePass() string { + return user.vault.BridgePass() +} + +func (user *User) UsedSpace() int { + return user.apiUser.UsedSpace +} + +func (user *User) MaxSpace() int { + return user.apiUser.MaxSpace +} + +// GetNotifyCh returns a channel which notifies of events happening to the user (such as deauth, address change) +func (user *User) GetNotifyCh() <-chan events.Event { + return user.notifyCh +} + +func (user *User) NewGluonConnector(ctx context.Context) (connector.Connector, error) { + if user.imapConn != nil { + if err := user.imapConn.Close(ctx); err != nil { + return nil, err + } + } + + user.imapConn = newIMAPConnector(user.client, user.updateCh, user.Addresses(), user.vault.BridgePass()) + + return user.imapConn, nil +} + +func (user *User) NewSMTPSession(username string) (smtp.Session, error) { + return newSMTPSession(user.client, username, user.addresses, user.userKR, user.addrKRs, user.settings), nil +} + +func (user *User) Logout(ctx context.Context) error { + return user.client.AuthDelete(ctx) +} + +func (user *User) Close(ctx context.Context) error { + // Close the user's IMAP connectors. + if user.imapConn != nil { + if err := user.imapConn.Close(ctx); err != nil { + return err + } + } + + // Close the user's message builder. + user.builder.Done() + + // Close the user's API client. + user.client.Close() + + // Close the user's notify channel. + close(user.notifyCh) + + return nil +} + +// sort returns the slice, sorted by the given callback. +func sort[T any](slice []T, less func(a, b T) bool) []T { + slices.SortFunc(slice, less) + + return slice +} diff --git a/internal/config/useragent/platform.go b/internal/useragent/platform.go similarity index 100% rename from internal/config/useragent/platform.go rename to internal/useragent/platform.go diff --git a/internal/config/useragent/platform_darwin.go b/internal/useragent/platform_darwin.go similarity index 100% rename from internal/config/useragent/platform_darwin.go rename to internal/useragent/platform_darwin.go diff --git a/internal/config/useragent/platform_default.go b/internal/useragent/platform_default.go similarity index 100% rename from internal/config/useragent/platform_default.go rename to internal/useragent/platform_default.go diff --git a/internal/config/useragent/platform_test.go b/internal/useragent/platform_test.go similarity index 100% rename from internal/config/useragent/platform_test.go rename to internal/useragent/platform_test.go diff --git a/internal/config/useragent/useragent.go b/internal/useragent/useragent.go similarity index 96% rename from internal/config/useragent/useragent.go rename to internal/useragent/useragent.go index 4db2a04b..17f0f184 100644 --- a/internal/config/useragent/useragent.go +++ b/internal/useragent/useragent.go @@ -46,7 +46,7 @@ func (ua *UserAgent) SetPlatform(platform string) { ua.platform = platform } -func (ua *UserAgent) String() string { +func (ua *UserAgent) GetUserAgent() string { var client string if ua.client != "" { diff --git a/internal/config/useragent/useragent_test.go b/internal/useragent/useragent_test.go similarity index 97% rename from internal/config/useragent/useragent_test.go rename to internal/useragent/useragent_test.go index 344f7982..d98ded4d 100644 --- a/internal/config/useragent/useragent_test.go +++ b/internal/useragent/useragent_test.go @@ -80,7 +80,7 @@ func TestUserAgent(t *testing.T) { ua.SetPlatform(test.platform) } - assert.Equal(t, test.want, ua.String()) + assert.Equal(t, test.want, ua.GetUserAgent()) }) } } diff --git a/internal/users/cache.go b/internal/users/cache.go deleted file mode 100644 index e7b63818..00000000 --- a/internal/users/cache.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "errors" - "io" - "os" - "path/filepath" - - "github.com/sirupsen/logrus" -) - -// isFolderEmpty checks whether a folder is empty. -// path must point to an existing folder. -func isFolderEmpty(path string) (bool, error) { - files, err := os.ReadDir(path) - if err != nil { - return true, err - } - return len(files) == 0, nil -} - -// checkFolderIsSuitableDestinationForCache determine if a folder is a suitable destination as a cache -// if it is suitable (non existing, or empty and deletable) the folder is deleted. -func checkFolderIsSuitableDestinationForCache(path string) error { - // Ensure the parent directory exists. - if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { - return err - } - - // if the folder does not exists, its suitable - fileInfo, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return err - } - - if !fileInfo.IsDir() { - return errors.New("the destination folder for message cache exists and is a file") - } - - empty, err := isFolderEmpty(path) - if err != nil { - return err - } - - if !empty { - return errors.New("the destination folder is not empty") - } - return os.Remove(path) -} - -// copyFolder recursively copy folder at srcPath to dstPath. -// srcPath must be an existing folder. -// dstPath must point to a non-existing folder. -func copyFolder(srcPath, dstPath string) error { - fiFrom, err := os.Stat(srcPath) - if err != nil { - return err - } - - _, err = os.Stat(dstPath) - if !os.IsNotExist(err) { - return errors.New("the destination folder already exists") - } - - if !fiFrom.IsDir() { - return errors.New("source is not an existing folder") - } - - if err = os.MkdirAll(dstPath, 0o700); err != nil { - return err - } - files, err := os.ReadDir(srcPath) - if err != nil { - return err - } - // copy only regular files and folders - for _, fileInfo := range files { - mode := fileInfo.Type() - if mode&os.ModeSymlink != 0 { - continue // we skip symbolic links to avoid potential endless recursion - } - srcSubPath := srcPath + "/" + fileInfo.Name() - dstSubPath := dstPath + "/" + fileInfo.Name() - - if mode.IsDir() { - if err = copyFolder(srcSubPath, dstSubPath); err != nil { - return err - } - continue - } - - if mode.IsRegular() { - if err = copyFile(srcSubPath, dstSubPath); err != nil { - return err - } - continue // unnecessary but safer if we had code below - } - } - return nil -} - -// isSubfolderOf check whether path is subfolder of refPath or is the same. -// RefPath must exist otherwise the function returns false. -func isSubfolderOf(path, refPath string) bool { - refInfo, err := os.Stat(refPath) - if (err != nil) || (!refInfo.IsDir()) { - return false // refpath does not exist. Not acceptable as we use os.SameFile for testing identity - } - - // we check path and all its parent folder to verify if it is refPath. - prevPath := "" - for path != prevPath { - pathInfo, err := os.Stat(path) // path may not exist, and it's acceptable, so wo keep going event if err != nil - if err == nil && os.SameFile(pathInfo, refInfo) { - return true - } - prevPath = path - path = filepath.Dir(path) - } - return false -} - -// copyFile copies file srcPath to dstPath. both path are files names. srcPath must exist, dstPath will be overwritten -// if it exists and is a file. -func copyFile(srcPath, dstPath string) error { - srcInfo, err := os.Stat(srcPath) - if err != nil { - return errors.New("could not open source file") - } - if !srcInfo.Mode().IsRegular() { - return errors.New("source file is not a regular file") - } - - dstInfo, err := os.Stat(dstPath) - if err == nil { - if !dstInfo.Mode().IsRegular() { - return errors.New("destination exists and is not a regular file") - } - if os.SameFile(srcInfo, dstInfo) { - return errors.New("source and destination are the same") - } - } - - src, err := os.Open(filepath.Clean(srcPath)) - if err != nil { - return err - } - defer func() { - err = src.Close() - }() - - dst, err := os.OpenFile(filepath.Clean(dstPath), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) - if err != nil { - return err - } - defer func() { - err = dst.Close() - }() - _, err = io.Copy(dst, src) - return err -} - -func (u *Users) EnableCache() error { - // NOTE(GODT-1158): Check for available size before enabling. - - return nil -} - -func (u *Users) DisableCache() error { - for _, user := range u.users { - if err := user.store.RemoveCache(); err != nil { - logrus.WithError(err).Error("Failed to remove user's message cache") - } - } - - return nil -} - -// MigrateCache moves the message cache folder from folder srcPath to folder dstPath. -// srcPath must point to an existing folder. dstPath must be an empty folder or not exist. -func (u *Users) MigrateCache(srcPath, dstPath string) error { - fiSrc, err := os.Stat(srcPath) - if os.IsNotExist(err) { - logrus.WithError(err).Warn("Skipping migration: unknown source for cache migration") - return nil - } - if !fiSrc.IsDir() { - logrus.WithError(err).Warn("Skipping migration: srcPath is not a dir") - return nil - } - - if isSubfolderOf(dstPath, srcPath) { - return errors.New("destination folder is a subfolder of the source folder") - } - - if err = checkFolderIsSuitableDestinationForCache(dstPath); err != nil { - logrus.WithError(err).Error("The destination folder is not suitable for cache migration") - return err - } - - for _, user := range u.users { - if err := user.closeStore(); err != nil { - logrus.WithError(err).Error("Failed to close user's store") - } - } - - // GODT-1381 Edge case: read-only source migration: prevent re-naming - // (read-only is conserved). Do copy instead. - tmp, err := os.CreateTemp(srcPath, "tmp") - if err == nil { - // Removal of tmp file cannot be deferred, as we are going to try to move the containing folder. - if err = tmp.Close(); err == nil { - if err = os.Remove(tmp.Name()); err == nil { - if err = os.Rename(srcPath, dstPath); err == nil { - return nil - } - } - } - } - - logrus.WithError(err).Warn("Cannot write to source: do copy to new destination instead of rename") - - // Rename failed let's try an actual copy/delete - if err = copyFolder(srcPath, dstPath); err != nil { - return err - } - - if err = os.RemoveAll(srcPath); err != nil { // we don't care much about error there. - logrus.WithError(err).Warn("Original cache folder could not be entirely removed") - } - - return nil -} diff --git a/internal/users/cache_test.go b/internal/users/cache_test.go deleted file mode 100644 index 29475c2f..00000000 --- a/internal/users/cache_test.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "crypto/sha1" - "encoding/hex" - "io" - "os" - "path/filepath" - "testing" - - r "github.com/stretchr/testify/require" -) - -const ( - str1 = "Lorem ipsum dolor sit amet" - str2 = "consectetur adipisicing elit" -) - -// tempFileWithContent() creates a temporary file in folderPath containing the string content. -// Returns the path of the created file. -func tempFileWithContent(folderPath, content string) (string, error) { - file, err := os.CreateTemp(folderPath, "") - if err != nil { - return "", err - } - defer func() { _ = file.Close() }() - _, err = file.WriteString(content) - return file.Name(), err -} - -// itemCountInFolder() counts the number of items (files, folders, etc) in a folder. -// Returns -1 if an error occurred. -func itemCountInFolder(path string) int { - files, err := os.ReadDir(path) - if err != nil { - return -1 - } - return len(files) -} - -// hashForFile returns the sha1 hash for the given file. -func hashForFile(path string) (string, error) { - hash := sha1.New() - file, err := os.Open(path) - if err != nil { - return "", err - } - defer func() { _ = file.Close() }() - if _, err = io.Copy(hash, file); err != nil { - return "", err - } - return hex.EncodeToString(hash.Sum(nil)), nil -} - -// filesAreIdentical() returns true if the two given files exist and have the same content. -func filesAreIdentical(path1, path2 string) bool { - hash1, err := hashForFile(path1) - if err != nil { - return false - } - hash2, err := hashForFile(path2) - return (err == nil) && hash1 == hash2 -} - -func TestCache_IsFolderEmpty(t *testing.T) { - _, err := isFolderEmpty("") - r.Error(t, err) - tempDirPath, err := os.MkdirTemp("", "") - defer func() { r.NoError(t, os.Remove(tempDirPath)) }() - r.NoError(t, err) - result, err := isFolderEmpty(tempDirPath) - r.NoError(t, err) - r.True(t, result) - tempFile, err := os.CreateTemp(tempDirPath, "") - r.NoError(t, err) - defer func() { r.NoError(t, os.Remove(tempFile.Name())) }() - r.NoError(t, tempFile.Close()) - _, err = isFolderEmpty(tempFile.Name()) - r.Error(t, err) - result, err = isFolderEmpty(tempDirPath) - r.NoError(t, err) - r.False(t, result) -} - -func TestCache_CheckFolderIsSuitableDestinationForCache(t *testing.T) { - tempDirPath, err := os.MkdirTemp("", "") - defer func() { _ = os.Remove(tempDirPath) }() // cleanup in case we fail before removing it. - r.NoError(t, err) - tempFile, err := os.CreateTemp(tempDirPath, "") - r.NoError(t, err) - defer func() { _ = os.Remove(tempFile.Name()) }() // cleanup in case we fail before removing it. - r.NoError(t, tempFile.Close()) - r.Error(t, checkFolderIsSuitableDestinationForCache(tempDirPath)) - r.NoError(t, os.Remove(tempFile.Name())) - r.NoError(t, checkFolderIsSuitableDestinationForCache(tempDirPath)) - r.NoDirExists(t, tempDirPath) // previous call to checkFolderIsSuitableDestinationForCache should have removed the folder - r.NoError(t, checkFolderIsSuitableDestinationForCache(tempDirPath)) -} - -func TestCache_CopyFolder(t *testing.T) { - // create a simple tree structure - // srcDir/ - // |-file1 - // |-srcSubDir/ - // |-file2 - - srcDir, err := os.MkdirTemp("", "") - defer func() { r.NoError(t, os.RemoveAll(srcDir)) }() - r.NoError(t, err) - srcSubDir, err := os.MkdirTemp(srcDir, "") - r.NoError(t, err) - subDirName := filepath.Base(srcSubDir) - file1, err := tempFileWithContent(srcDir, str1) - r.NoError(t, err) - file2, err := tempFileWithContent(srcSubDir, str2) - r.NoError(t, err) - - // copy it - dstDir := srcDir + "_" - r.NoDirExists(t, dstDir) - r.NoFileExists(t, dstDir) - r.Error(t, copyFolder(srcDir, srcDir)) - r.NoError(t, copyFolder(srcDir, dstDir)) - defer func() { r.NoError(t, os.RemoveAll(dstDir)) }() - - // check copy and original - r.DirExists(t, srcDir) - r.DirExists(t, srcSubDir) - r.FileExists(t, file1) - r.FileExists(t, file2) - r.True(t, itemCountInFolder(srcDir) == 2) - r.True(t, itemCountInFolder(srcSubDir) == 1) - r.DirExists(t, dstDir) - dstSubDir := filepath.Join(dstDir, subDirName) - r.DirExists(t, dstSubDir) - dstFile1 := filepath.Join(dstDir, filepath.Base(file1)) - r.FileExists(t, dstFile1) - dstFile2 := filepath.Join(dstDir, subDirName, filepath.Base(file2)) - r.FileExists(t, dstFile2) - r.True(t, itemCountInFolder(dstDir) == 2) - r.True(t, itemCountInFolder(dstSubDir) == 1) - r.True(t, filesAreIdentical(file1, dstFile1)) - r.True(t, filesAreIdentical(file2, dstFile2)) -} - -func TestCache_IsSubfolderOf(t *testing.T) { - dir, err := os.MkdirTemp("", "") - defer func() { r.NoError(t, os.Remove(dir)) }() - r.NoError(t, err) - r.True(t, isSubfolderOf(dir, dir)) - fakeDir := dir + "_" - r.False(t, isSubfolderOf(dir, fakeDir+"_")) - subDir := filepath.Join(dir, "A", "B") - r.True(t, isSubfolderOf(subDir, dir)) - r.True(t, isSubfolderOf(filepath.Dir(subDir), dir)) - r.False(t, isSubfolderOf(dir, subDir)) -} - -func TestCache_CopyFile(t *testing.T) { - file1, err := tempFileWithContent("", str1) - r.NoError(t, err) - defer func() { r.NoError(t, os.Remove(file1)) }() - file2, err := tempFileWithContent("", str2) - r.NoError(t, err) - defer func() { r.NoError(t, os.Remove(file2)) }() - r.Error(t, copyFile(file1, file1)) - r.Error(t, copyFile(file1, filepath.Dir(file1))) - r.Error(t, copyFile(file1, file1)) - r.NoError(t, copyFile(file1, file2)) - file3 := file2 + "_" - r.NoFileExists(t, file3) - r.NoError(t, copyFile(file1, file3)) - defer func() { r.NoError(t, os.Remove(file3)) }() -} diff --git a/internal/users/credentials/credentials.go b/internal/users/credentials/credentials.go deleted file mode 100644 index 30801ebe..00000000 --- a/internal/users/credentials/credentials.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package credentials implements our struct stored in keychain. -// Store struct is kind of like a database client. -// Credentials struct is kind of like one record from the database. -package credentials - -import ( - "crypto/subtle" - "encoding/base64" - "errors" - "fmt" - "strings" - - "github.com/sirupsen/logrus" -) - -const ( - sep = "\x00" - - itemLengthBridge = 9 - itemLengthImportExport = 6 // Old format for Import-Export. -) - -var ( - log = logrus.WithField("pkg", "credentials") //nolint:gochecknoglobals - - ErrWrongFormat = errors.New("malformed credentials") -) - -type Credentials struct { - UserID, // Do not marshal; used as a key. - Name, - Emails, - APIToken string - MailboxPassword []byte - BridgePassword, - Version string - Timestamp int64 - IsHidden, // Deprecated. - IsCombinedAddressMode bool -} - -func (s *Credentials) Marshal() string { - items := []string{ - s.Name, // 0 - s.Emails, // 1 - s.APIToken, // 2 - string(s.MailboxPassword), // 3 - s.BridgePassword, // 4 - s.Version, // 5 - "", // 6 - "", // 7 - "", // 8 - } - - items[6] = fmt.Sprint(s.Timestamp) - - if s.IsHidden { - items[7] = "1" - } - - if s.IsCombinedAddressMode { - items[8] = "1" - } - - str := strings.Join(items, sep) - return base64.StdEncoding.EncodeToString([]byte(str)) -} - -func (s *Credentials) Unmarshal(secret string) error { - b, err := base64.StdEncoding.DecodeString(secret) - if err != nil { - return err - } - items := strings.Split(string(b), sep) - - if len(items) != itemLengthBridge && len(items) != itemLengthImportExport { - return ErrWrongFormat - } - - s.Name = items[0] - s.Emails = items[1] - s.APIToken = items[2] - s.MailboxPassword = []byte(items[3]) - - switch len(items) { - case itemLengthBridge: - s.BridgePassword = items[4] - s.Version = items[5] - if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil { - s.Timestamp = 0 - } - if s.IsHidden = false; items[7] == "1" { - s.IsHidden = true - } - if s.IsCombinedAddressMode = false; items[8] == "1" { - s.IsCombinedAddressMode = true - } - - case itemLengthImportExport: - s.Version = items[4] - if _, err = fmt.Sscan(items[5], &s.Timestamp); err != nil { - s.Timestamp = 0 - } - } - return nil -} - -func (s *Credentials) SetEmailList(list []string) { - s.Emails = strings.Join(list, ";") -} - -func (s *Credentials) EmailList() []string { - return strings.Split(s.Emails, ";") -} - -func (s *Credentials) CheckPassword(password string) error { - if subtle.ConstantTimeCompare([]byte(s.BridgePassword), []byte(password)) != 1 { - log.WithFields(logrus.Fields{ - "userID": s.UserID, - }).Debug("Incorrect bridge password") - - return fmt.Errorf("backend/credentials: incorrect password") - } - return nil -} - -func (s *Credentials) Logout() { - s.APIToken = "" - - for i := range s.MailboxPassword { - s.MailboxPassword[i] = 0 - } - - s.MailboxPassword = []byte{} -} - -func (s *Credentials) IsConnected() bool { - return s.APIToken != "" && len(s.MailboxPassword) != 0 -} - -func (s *Credentials) SplitAPIToken() (string, string, error) { - split := strings.Split(s.APIToken, ":") - - if len(split) != 2 { - return "", "", errors.New("malformed API token") - } - - return split[0], split[1], nil -} diff --git a/internal/users/credentials/credentials_test.go b/internal/users/credentials/credentials_test.go deleted file mode 100644 index fb8460b7..00000000 --- a/internal/users/credentials/credentials_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package credentials - -import ( - "encoding/base64" - "fmt" - "strings" - "testing" - "time" - - r "github.com/stretchr/testify/require" -) - -var wantCredentials = Credentials{ - UserID: "1", - Name: "name", - Emails: "email1;email2", - APIToken: "token", - MailboxPassword: []byte("mailbox pass"), - BridgePassword: "bridge pass", - Version: "k11", - Timestamp: time.Now().Unix(), - IsHidden: false, - IsCombinedAddressMode: false, -} - -func TestUnmarshallBridge(t *testing.T) { - encoded := wantCredentials.Marshal() - haveCredentials := Credentials{UserID: "1"} - r.NoError(t, haveCredentials.Unmarshal(encoded)) - r.Equal(t, wantCredentials, haveCredentials) -} - -func TestUnmarshallImportExport(t *testing.T) { - items := []string{ - wantCredentials.Name, - wantCredentials.Emails, - wantCredentials.APIToken, - string(wantCredentials.MailboxPassword), - "k11", - fmt.Sprint(wantCredentials.Timestamp), - } - - str := strings.Join(items, sep) - encoded := base64.StdEncoding.EncodeToString([]byte(str)) - - haveCredentials := Credentials{UserID: "1"} - haveCredentials.BridgePassword = wantCredentials.BridgePassword // This one is not used. - r.NoError(t, haveCredentials.Unmarshal(encoded)) - r.Equal(t, wantCredentials, haveCredentials) -} diff --git a/internal/users/credentials/pass.go b/internal/users/credentials/pass.go deleted file mode 100644 index 78211284..00000000 --- a/internal/users/credentials/pass.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build !imaptest -// +build !imaptest - -package credentials - -import ( - "crypto/rand" - "encoding/base64" - "io" -) - -const keySize = 16 - -// generateKey generates a new random key. -func generateKey() []byte { - key := make([]byte, keySize) - if _, err := io.ReadFull(rand.Reader, key); err != nil { - panic(err) - } - return key -} - -func generatePassword() string { - return base64.RawURLEncoding.EncodeToString(generateKey()) -} diff --git a/internal/users/credentials/pass_imaptest.go b/internal/users/credentials/pass_imaptest.go deleted file mode 100644 index 4cf5eea6..00000000 --- a/internal/users/credentials/pass_imaptest.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build imaptest -// +build imaptest - -package credentials - -func generatePassword() string { - return "abcdefgh12345678" -} diff --git a/internal/users/credentials/store.go b/internal/users/credentials/store.go deleted file mode 100644 index 2b4918cd..00000000 --- a/internal/users/credentials/store.go +++ /dev/null @@ -1,273 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package credentials - -import ( - "errors" - "fmt" - "sort" - "sync" - "time" - - "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" - "github.com/sirupsen/logrus" -) - -var storeLocker = sync.RWMutex{} //nolint:gochecknoglobals - -// Store is an encrypted credentials store. -type Store struct { - secrets *keychain.Keychain -} - -// NewStore creates a new encrypted credentials store. -func NewStore(keychain *keychain.Keychain) *Store { - return &Store{secrets: keychain} -} - -func (s *Store) Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*Credentials, error) { - storeLocker.Lock() - defer storeLocker.Unlock() - - log.WithFields(logrus.Fields{ - "user": userID, - "username": userName, - "emails": emails, - }).Trace("Adding new credentials") - - creds := &Credentials{ - UserID: userID, - Name: userName, - APIToken: uid + ":" + ref, - MailboxPassword: mailboxPassword, - IsHidden: false, - } - - creds.SetEmailList(emails) - - currentCredentials, err := s.get(userID) - if err == nil { - log.Info("Updating credentials of existing user") - creds.BridgePassword = currentCredentials.BridgePassword - creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode - creds.Timestamp = currentCredentials.Timestamp - } else { - log.Info("Generating credentials for new user") - creds.BridgePassword = generatePassword() - creds.IsCombinedAddressMode = true - creds.Timestamp = time.Now().Unix() - } - - if err := s.saveCredentials(creds); err != nil { - return nil, err - } - - return creds, nil -} - -func (s *Store) SwitchAddressMode(userID string) (*Credentials, error) { - storeLocker.Lock() - defer storeLocker.Unlock() - - credentials, err := s.get(userID) - if err != nil { - return nil, err - } - - credentials.IsCombinedAddressMode = !credentials.IsCombinedAddressMode - credentials.BridgePassword = generatePassword() - - return credentials, s.saveCredentials(credentials) -} - -func (s *Store) UpdateEmails(userID string, emails []string) (*Credentials, error) { - storeLocker.Lock() - defer storeLocker.Unlock() - - credentials, err := s.get(userID) - if err != nil { - return nil, err - } - - credentials.SetEmailList(emails) - - return credentials, s.saveCredentials(credentials) -} - -func (s *Store) UpdatePassword(userID string, password []byte) (*Credentials, error) { - storeLocker.Lock() - defer storeLocker.Unlock() - - credentials, err := s.get(userID) - if err != nil { - return nil, err - } - - credentials.MailboxPassword = password - - return credentials, s.saveCredentials(credentials) -} - -func (s *Store) UpdateToken(userID, uid, ref string) (*Credentials, error) { - storeLocker.Lock() - defer storeLocker.Unlock() - - credentials, err := s.get(userID) - if err != nil { - return nil, err - } - - credentials.APIToken = uid + ":" + ref - - return credentials, s.saveCredentials(credentials) -} - -func (s *Store) Logout(userID string) (*Credentials, error) { - storeLocker.Lock() - defer storeLocker.Unlock() - - credentials, err := s.get(userID) - if err != nil { - return nil, err - } - - credentials.Logout() - - return credentials, s.saveCredentials(credentials) -} - -// List returns a list of usernames that have credentials stored. -func (s *Store) List() (userIDs []string, err error) { - storeLocker.RLock() - defer storeLocker.RUnlock() - - log.Trace("Listing credentials in credentials store") - - var allUserIDs []string - if allUserIDs, err = s.secrets.List(); err != nil { - log.WithError(err).Error("Could not list credentials") - return - } - - credentialList := []*Credentials{} - for _, userID := range allUserIDs { - creds, getErr := s.get(userID) - if getErr != nil { - log.WithField("userID", userID).WithError(getErr).Warn("Failed to get credentials") - continue - } - - // Disabled credentials - if creds.Timestamp == 0 { - continue - } - - // Old credentials using username as a key does not work with new code. - // We need to ask user to login again to get ID from API and migrate creds. - if creds.UserID == creds.Name && creds.APIToken != "" { - creds.Logout() - _ = s.saveCredentials(creds) - } - - credentialList = append(credentialList, creds) - } - - sort.Slice(credentialList, func(i, j int) bool { - return credentialList[i].Timestamp < credentialList[j].Timestamp - }) - - for _, credentials := range credentialList { - userIDs = append(userIDs, credentials.UserID) - } - - return userIDs, err -} - -func (s *Store) GetAndCheckPassword(userID, password string) (creds *Credentials, err error) { - storeLocker.RLock() - defer storeLocker.RUnlock() - - log.WithFields(logrus.Fields{ - "userID": userID, - }).Debug("Checking bridge password") - - credentials, err := s.Get(userID) - if err != nil { - return nil, err - } - - if err := credentials.CheckPassword(password); err != nil { - log.WithFields(logrus.Fields{ - "userID": userID, - "err": err, - }).Debug("Incorrect bridge password") - - return nil, err - } - - return credentials, nil -} - -func (s *Store) Get(userID string) (creds *Credentials, err error) { - storeLocker.RLock() - defer storeLocker.RUnlock() - - return s.get(userID) -} - -func (s *Store) get(userID string) (*Credentials, error) { - log := log.WithField("user", userID) - - _, secret, err := s.secrets.Get(userID) - if err != nil { - return nil, err - } - - if secret == "" { - return nil, errors.New("secret is empty") - } - - credentials := &Credentials{UserID: userID} - - if err := credentials.Unmarshal(secret); err != nil { - log.WithError(fmt.Errorf("malformed secret: %w", err)).Error("Could not unmarshal secret") - - if err := s.secrets.Delete(userID); err != nil { - log.WithError(err).Error("Failed to remove malformed secret") - } - - return nil, err - } - - return credentials, nil -} - -// saveCredentials encrypts and saves password to the keychain store. -func (s *Store) saveCredentials(credentials *Credentials) error { - credentials.Version = keychain.Version - - return s.secrets.Put(credentials.UserID, credentials.Marshal()) -} - -// Delete removes credentials from the store. -func (s *Store) Delete(userID string) (err error) { - storeLocker.Lock() - defer storeLocker.Unlock() - - return s.secrets.Delete(userID) -} diff --git a/internal/users/credentials/store_test.go b/internal/users/credentials/store_test.go deleted file mode 100644 index 1b96deff..00000000 --- a/internal/users/credentials/store_test.go +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package credentials - -import ( - "bytes" - "encoding/base64" - "encoding/gob" - "encoding/json" - "fmt" - "strings" - "testing" - - r "github.com/stretchr/testify/require" -) - -const ( - testSep = "\n" - secretFormat = "%v" + testSep + // UserID, - "%v" + testSep + // Name, - "%v" + testSep + // Emails, - "%v" + testSep + // APIToken, - "%v" + testSep + // Mailbox, - "%v" + testSep + // BridgePassword, - "%v" + testSep + // Version string - "%v" + testSep + // Timestamp, - "%v" + testSep + // IsHidden, - "%v" // IsCombinedAddressMode -) - -// the best would be to run this test on mac, win, and linux separately - -type testCredentials struct { - UserID, - Name, - Emails, - APIToken, - Mailbox, - BridgePassword, - Version string - Timestamp int64 - IsHidden, - IsCombinedAddressMode bool -} - -func init() { //nolint:gochecknoinits - gob.Register(testCredentials{}) -} - -func (s *testCredentials) MarshalGob() string { - buf := bytes.Buffer{} - enc := gob.NewEncoder(&buf) - if err := enc.Encode(s); err != nil { - return "" - } - log.Infof("MarshalGob: %#v\n", buf.String()) - return base64.StdEncoding.EncodeToString(buf.Bytes()) -} - -func (s *testCredentials) Clear() { - s.UserID = "" - s.Name = "" - s.Emails = "" - s.APIToken = "" - s.Mailbox = "" - s.BridgePassword = "" - s.Version = "" - s.Timestamp = 0 - s.IsHidden = false - s.IsCombinedAddressMode = false -} - -func (s *testCredentials) UnmarshalGob(secret string) error { - s.Clear() - b, err := base64.StdEncoding.DecodeString(secret) - if err != nil { - log.Infoln("decode base64", b) - return err - } - buf := bytes.NewBuffer(b) - dec := gob.NewDecoder(buf) - if err = dec.Decode(s); err != nil { - log.Info("decode gob", b, buf.Bytes()) - return err - } - return nil -} - -func (s *testCredentials) ToJSON() string { - if b, err := json.Marshal(s); err == nil { - log.Infof("MarshalJSON: %#v\n", string(b)) - return base64.StdEncoding.EncodeToString(b) - } - return "" -} - -func (s *testCredentials) FromJSON(secret string) error { - b, err := base64.StdEncoding.DecodeString(secret) - if err != nil { - return err - } - if err = json.Unmarshal(b, s); err == nil { - return nil - } - return err -} - -func (s *testCredentials) MarshalFmt() string { - buf := bytes.Buffer{} - fmt.Fprintf( - &buf, secretFormat, - s.UserID, - s.Name, - s.Emails, - s.APIToken, - s.Mailbox, - s.BridgePassword, - s.Version, - s.Timestamp, - s.IsHidden, - s.IsCombinedAddressMode, - ) - log.Infof("MarshalFmt: %#v\n", buf.String()) - return base64.StdEncoding.EncodeToString(buf.Bytes()) -} - -func (s *testCredentials) UnmarshalFmt(secret string) error { - b, err := base64.StdEncoding.DecodeString(secret) - if err != nil { - return err - } - buf := bytes.NewBuffer(b) - log.Infoln("decode fmt", b, buf.Bytes()) - _, err = fmt.Fscanf( - buf, secretFormat, - &s.UserID, - &s.Name, - &s.Emails, - &s.APIToken, - &s.Mailbox, - &s.BridgePassword, - &s.Version, - &s.Timestamp, - &s.IsHidden, - &s.IsCombinedAddressMode, - ) - if err != nil { - return err - } - return nil -} - -func (s *testCredentials) MarshalStrings() string { // this is the most space efficient - items := []string{ - s.UserID, // 0 - s.Name, // 1 - s.Emails, // 2 - s.APIToken, // 3 - s.Mailbox, // 4 - s.BridgePassword, // 5 - s.Version, // 6 - } - items = append(items, fmt.Sprint(s.Timestamp)) // 7 - - if s.IsHidden { // 8 - items = append(items, "1") - } else { - items = append(items, "") - } - - if s.IsCombinedAddressMode { // 9 - items = append(items, "1") - } else { - items = append(items, "") - } - - str := strings.Join(items, sep) - - log.Infof("MarshalJoin: %#v\n", str) - return base64.StdEncoding.EncodeToString([]byte(str)) -} - -func (s *testCredentials) UnmarshalStrings(secret string) error { - b, err := base64.StdEncoding.DecodeString(secret) - if err != nil { - return err - } - items := strings.Split(string(b), sep) - if len(items) != 10 { - return ErrWrongFormat - } - - s.UserID = items[0] - s.Name = items[1] - s.Emails = items[2] - s.APIToken = items[3] - s.Mailbox = items[4] - s.BridgePassword = items[5] - s.Version = items[6] - if _, err = fmt.Sscanf(items[7], "%d", &s.Timestamp); err != nil { - s.Timestamp = 0 - } - if s.IsHidden = false; items[8] == "1" { - s.IsHidden = true - } - if s.IsCombinedAddressMode = false; items[9] == "1" { - s.IsCombinedAddressMode = true - } - return nil -} - -func (s *testCredentials) IsSame(rhs *testCredentials) bool { - return s.Name == rhs.Name && - s.Emails == rhs.Emails && - s.APIToken == rhs.APIToken && - s.Mailbox == rhs.Mailbox && - s.BridgePassword == rhs.BridgePassword && - s.Version == rhs.Version && - s.Timestamp == rhs.Timestamp && - s.IsHidden == rhs.IsHidden && - s.IsCombinedAddressMode == rhs.IsCombinedAddressMode -} - -func TestMarshalFormats(t *testing.T) { - input := testCredentials{UserID: "007", Emails: "ja@pm.me;jakub@cu.th", Timestamp: 152469263742, IsHidden: true} - log.Infof("input %#v\n", input) - - secretStrings := input.MarshalStrings() - log.Infof("secretStrings %#v %d\n", secretStrings, len(secretStrings)) - secretGob := input.MarshalGob() - log.Infof("secretGob %#v %d\n", secretGob, len(secretGob)) - secretJSON := input.ToJSON() - log.Infof("secretJSON %#v %d\n", secretJSON, len(secretJSON)) - secretFmt := input.MarshalFmt() - log.Infof("secretFmt %#v %d\n", secretFmt, len(secretFmt)) - - output := testCredentials{APIToken: "refresh"} - r.NoError(t, output.UnmarshalStrings(secretStrings)) - log.Infof("strings out %#v \n", output) - r.True(t, input.IsSame(&output), "strings out not same") - - output = testCredentials{APIToken: "refresh"} - r.NoError(t, output.UnmarshalGob(secretGob)) - log.Infof("gob out %#v\n \n", output) - r.Equal(t, input, output) - - output = testCredentials{APIToken: "refresh"} - r.NoError(t, output.FromJSON(secretJSON)) - log.Infof("json out %#v \n", output) - r.True(t, input.IsSame(&output), "json out not same") - - /* - // Simple Fscanf not working! - output = testCredentials{APIToken: "refresh"} - r.NoError(t, output.UnmarshalFmt(secretFmt)) - log.Infof("fmt out %#v \n", output) - r.True(t, input.IsSame(&output), "fmt out not same") - */ -} - -func TestMarshal(t *testing.T) { - input := Credentials{ - UserID: "", - Name: "007", - Emails: "ja@pm.me;aj@cus.tom", - APIToken: "sdfdsfsdfsdfsdf", - MailboxPassword: []byte("cdcdcdcd"), - BridgePassword: "wew123", - Version: "k11", - Timestamp: 152469263742, - IsHidden: true, - IsCombinedAddressMode: false, - } - log.Infof("input %#v\n", input) - - secret := input.Marshal() - log.Infof("secret %#v %d\n", secret, len(secret)) - - output := Credentials{APIToken: "refresh"} - r.NoError(t, output.Unmarshal(secret)) - log.Infof("output %#v\n", output) - r.Equal(t, input, output) -} diff --git a/internal/users/mocks/listener_mocks.go b/internal/users/mocks/listener_mocks.go deleted file mode 100644 index e60e9f03..00000000 --- a/internal/users/mocks/listener_mocks.go +++ /dev/null @@ -1,133 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ProtonMail/proton-bridge/v2/pkg/listener (interfaces: Listener) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - time "time" - - gomock "github.com/golang/mock/gomock" -) - -// MockListener is a mock of Listener interface. -type MockListener struct { - ctrl *gomock.Controller - recorder *MockListenerMockRecorder -} - -// MockListenerMockRecorder is the mock recorder for MockListener. -type MockListenerMockRecorder struct { - mock *MockListener -} - -// NewMockListener creates a new mock instance. -func NewMockListener(ctrl *gomock.Controller) *MockListener { - mock := &MockListener{ctrl: ctrl} - mock.recorder = &MockListenerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockListener) EXPECT() *MockListenerMockRecorder { - return m.recorder -} - -// Add mocks base method. -func (m *MockListener) Add(arg0 string, arg1 chan<- string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Add", arg0, arg1) -} - -// Add indicates an expected call of Add. -func (mr *MockListenerMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockListener)(nil).Add), arg0, arg1) -} - -// Book mocks base method. -func (m *MockListener) Book(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Book", arg0) -} - -// Book indicates an expected call of Book. -func (mr *MockListenerMockRecorder) Book(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Book", reflect.TypeOf((*MockListener)(nil).Book), arg0) -} - -// Emit mocks base method. -func (m *MockListener) Emit(arg0, arg1 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Emit", arg0, arg1) -} - -// Emit indicates an expected call of Emit. -func (mr *MockListenerMockRecorder) Emit(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), arg0, arg1) -} - -// ProvideChannel mocks base method. -func (m *MockListener) ProvideChannel(arg0 string) <-chan string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProvideChannel", arg0) - ret0, _ := ret[0].(<-chan string) - return ret0 -} - -// ProvideChannel indicates an expected call of ProvideChannel. -func (mr *MockListenerMockRecorder) ProvideChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProvideChannel", reflect.TypeOf((*MockListener)(nil).ProvideChannel), arg0) -} - -// Remove mocks base method. -func (m *MockListener) Remove(arg0 string, arg1 chan<- string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Remove", arg0, arg1) -} - -// Remove indicates an expected call of Remove. -func (mr *MockListenerMockRecorder) Remove(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockListener)(nil).Remove), arg0, arg1) -} - -// RetryEmit mocks base method. -func (m *MockListener) RetryEmit(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "RetryEmit", arg0) -} - -// RetryEmit indicates an expected call of RetryEmit. -func (mr *MockListenerMockRecorder) RetryEmit(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryEmit", reflect.TypeOf((*MockListener)(nil).RetryEmit), arg0) -} - -// SetBuffer mocks base method. -func (m *MockListener) SetBuffer(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetBuffer", arg0) -} - -// SetBuffer indicates an expected call of SetBuffer. -func (mr *MockListenerMockRecorder) SetBuffer(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBuffer", reflect.TypeOf((*MockListener)(nil).SetBuffer), arg0) -} - -// SetLimit mocks base method. -func (m *MockListener) SetLimit(arg0 string, arg1 time.Duration) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetLimit", arg0, arg1) -} - -// SetLimit indicates an expected call of SetLimit. -func (mr *MockListenerMockRecorder) SetLimit(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockListener)(nil).SetLimit), arg0, arg1) -} diff --git a/internal/users/mocks/mocks.go b/internal/users/mocks/mocks.go deleted file mode 100644 index 0c69bfa8..00000000 --- a/internal/users/mocks/mocks.go +++ /dev/null @@ -1,294 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ProtonMail/proton-bridge/v2/internal/users (interfaces: Locator,PanicHandler,CredentialsStorer,StoreMaker) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - store "github.com/ProtonMail/proton-bridge/v2/internal/store" - credentials "github.com/ProtonMail/proton-bridge/v2/internal/users/credentials" - gomock "github.com/golang/mock/gomock" -) - -// MockLocator is a mock of Locator interface. -type MockLocator struct { - ctrl *gomock.Controller - recorder *MockLocatorMockRecorder -} - -// MockLocatorMockRecorder is the mock recorder for MockLocator. -type MockLocatorMockRecorder struct { - mock *MockLocator -} - -// NewMockLocator creates a new mock instance. -func NewMockLocator(ctrl *gomock.Controller) *MockLocator { - mock := &MockLocator{ctrl: ctrl} - mock.recorder = &MockLocatorMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockLocator) EXPECT() *MockLocatorMockRecorder { - return m.recorder -} - -// Clear mocks base method. -func (m *MockLocator) Clear() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Clear") - ret0, _ := ret[0].(error) - return ret0 -} - -// Clear indicates an expected call of Clear. -func (mr *MockLocatorMockRecorder) Clear() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clear", reflect.TypeOf((*MockLocator)(nil).Clear)) -} - -// MockPanicHandler is a mock of PanicHandler interface. -type MockPanicHandler struct { - ctrl *gomock.Controller - recorder *MockPanicHandlerMockRecorder -} - -// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler. -type MockPanicHandlerMockRecorder struct { - mock *MockPanicHandler -} - -// NewMockPanicHandler creates a new mock instance. -func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler { - mock := &MockPanicHandler{ctrl: ctrl} - mock.recorder = &MockPanicHandlerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder { - return m.recorder -} - -// HandlePanic mocks base method. -func (m *MockPanicHandler) HandlePanic() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "HandlePanic") -} - -// HandlePanic indicates an expected call of HandlePanic. -func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic)) -} - -// MockCredentialsStorer is a mock of CredentialsStorer interface. -type MockCredentialsStorer struct { - ctrl *gomock.Controller - recorder *MockCredentialsStorerMockRecorder -} - -// MockCredentialsStorerMockRecorder is the mock recorder for MockCredentialsStorer. -type MockCredentialsStorerMockRecorder struct { - mock *MockCredentialsStorer -} - -// NewMockCredentialsStorer creates a new mock instance. -func NewMockCredentialsStorer(ctrl *gomock.Controller) *MockCredentialsStorer { - mock := &MockCredentialsStorer{ctrl: ctrl} - mock.recorder = &MockCredentialsStorerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCredentialsStorer) EXPECT() *MockCredentialsStorerMockRecorder { - return m.recorder -} - -// Add mocks base method. -func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3 string, arg4 []byte, arg5 []string) (*credentials.Credentials, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Add", arg0, arg1, arg2, arg3, arg4, arg5) - ret0, _ := ret[0].(*credentials.Credentials) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Add indicates an expected call of Add. -func (mr *MockCredentialsStorerMockRecorder) Add(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockCredentialsStorer)(nil).Add), arg0, arg1, arg2, arg3, arg4, arg5) -} - -// Delete mocks base method. -func (m *MockCredentialsStorer) Delete(arg0 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockCredentialsStorerMockRecorder) Delete(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCredentialsStorer)(nil).Delete), arg0) -} - -// Get mocks base method. -func (m *MockCredentialsStorer) Get(arg0 string) (*credentials.Credentials, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", arg0) - ret0, _ := ret[0].(*credentials.Credentials) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get. -func (mr *MockCredentialsStorerMockRecorder) Get(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCredentialsStorer)(nil).Get), arg0) -} - -// List mocks base method. -func (m *MockCredentialsStorer) List() ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// List indicates an expected call of List. -func (mr *MockCredentialsStorerMockRecorder) List() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCredentialsStorer)(nil).List)) -} - -// Logout mocks base method. -func (m *MockCredentialsStorer) Logout(arg0 string) (*credentials.Credentials, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Logout", arg0) - ret0, _ := ret[0].(*credentials.Credentials) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Logout indicates an expected call of Logout. -func (mr *MockCredentialsStorerMockRecorder) Logout(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockCredentialsStorer)(nil).Logout), arg0) -} - -// SwitchAddressMode mocks base method. -func (m *MockCredentialsStorer) SwitchAddressMode(arg0 string) (*credentials.Credentials, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SwitchAddressMode", arg0) - ret0, _ := ret[0].(*credentials.Credentials) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SwitchAddressMode indicates an expected call of SwitchAddressMode. -func (mr *MockCredentialsStorerMockRecorder) SwitchAddressMode(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwitchAddressMode", reflect.TypeOf((*MockCredentialsStorer)(nil).SwitchAddressMode), arg0) -} - -// UpdateEmails mocks base method. -func (m *MockCredentialsStorer) UpdateEmails(arg0 string, arg1 []string) (*credentials.Credentials, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateEmails", arg0, arg1) - ret0, _ := ret[0].(*credentials.Credentials) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateEmails indicates an expected call of UpdateEmails. -func (mr *MockCredentialsStorerMockRecorder) UpdateEmails(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmails", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateEmails), arg0, arg1) -} - -// UpdatePassword mocks base method. -func (m *MockCredentialsStorer) UpdatePassword(arg0 string, arg1 []byte) (*credentials.Credentials, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePassword", arg0, arg1) - ret0, _ := ret[0].(*credentials.Credentials) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdatePassword indicates an expected call of UpdatePassword. -func (mr *MockCredentialsStorerMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdatePassword), arg0, arg1) -} - -// UpdateToken mocks base method. -func (m *MockCredentialsStorer) UpdateToken(arg0, arg1, arg2 string) (*credentials.Credentials, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateToken", arg0, arg1, arg2) - ret0, _ := ret[0].(*credentials.Credentials) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateToken indicates an expected call of UpdateToken. -func (mr *MockCredentialsStorerMockRecorder) UpdateToken(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateToken", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateToken), arg0, arg1, arg2) -} - -// MockStoreMaker is a mock of StoreMaker interface. -type MockStoreMaker struct { - ctrl *gomock.Controller - recorder *MockStoreMakerMockRecorder -} - -// MockStoreMakerMockRecorder is the mock recorder for MockStoreMaker. -type MockStoreMakerMockRecorder struct { - mock *MockStoreMaker -} - -// NewMockStoreMaker creates a new mock instance. -func NewMockStoreMaker(ctrl *gomock.Controller) *MockStoreMaker { - mock := &MockStoreMaker{ctrl: ctrl} - mock.recorder = &MockStoreMakerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStoreMaker) EXPECT() *MockStoreMakerMockRecorder { - return m.recorder -} - -// New mocks base method. -func (m *MockStoreMaker) New(arg0 store.BridgeUser) (*store.Store, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "New", arg0) - ret0, _ := ret[0].(*store.Store) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// New indicates an expected call of New. -func (mr *MockStoreMakerMockRecorder) New(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockStoreMaker)(nil).New), arg0) -} - -// Remove mocks base method. -func (m *MockStoreMaker) Remove(arg0 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Remove", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Remove indicates an expected call of Remove. -func (mr *MockStoreMakerMockRecorder) Remove(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockStoreMaker)(nil).Remove), arg0) -} diff --git a/internal/users/types.go b/internal/users/types.go deleted file mode 100644 index b4bd74b7..00000000 --- a/internal/users/types.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "github.com/ProtonMail/proton-bridge/v2/internal/store" - "github.com/ProtonMail/proton-bridge/v2/internal/users/credentials" -) - -type Locator interface { - Clear() error -} - -type PanicHandler interface { - HandlePanic() -} - -type CredentialsStorer interface { - List() (userIDs []string, err error) - Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*credentials.Credentials, error) - Get(userID string) (*credentials.Credentials, error) - SwitchAddressMode(userID string) (*credentials.Credentials, error) - UpdateEmails(userID string, emails []string) (*credentials.Credentials, error) - UpdatePassword(userID string, password []byte) (*credentials.Credentials, error) - UpdateToken(userID, uid, ref string) (*credentials.Credentials, error) - Logout(userID string) (*credentials.Credentials, error) - Delete(userID string) error -} - -type StoreMaker interface { - New(user store.BridgeUser) (*store.Store, error) - Remove(userID string) error -} - -type UserInfo struct { - ID string - Username string - Password string - - Addresses []string - Primary int - - UsedBytes int64 - TotalBytes int64 - - Connected bool - Mode AddressMode -} - -type AddressMode int - -const ( - SplitMode AddressMode = iota - CombinedMode -) - -func (mode AddressMode) String() string { - switch mode { - case SplitMode: - return "split mode" - - case CombinedMode: - return "combined mode" - - default: - return "unknown mode" - } -} diff --git a/internal/users/user.go b/internal/users/user.go deleted file mode 100644 index 70bee322..00000000 --- a/internal/users/user.go +++ /dev/null @@ -1,544 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "context" - "runtime" - "strings" - "sync" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/store" - "github.com/ProtonMail/proton-bridge/v2/internal/users/credentials" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from the app. -var ErrLoggedOutUser = errors.New("account is logged out, use the app to login again") - -// User is a struct on top of API client and credentials store. -type User struct { - log *logrus.Entry - panicHandler PanicHandler - listener listener.Listener - client pmapi.Client - credStorer CredentialsStorer - - storeFactory StoreMaker - store *store.Store - - userID string - creds *credentials.Credentials - - usedBytes, totalBytes int64 - - lock sync.RWMutex -} - -// newUser creates a new user. -// The user is initially disconnected and must be connected by calling connect(). -func newUser( - panicHandler PanicHandler, - userID string, - eventListener listener.Listener, - credStorer CredentialsStorer, - storeFactory StoreMaker, -) (*User, *credentials.Credentials, error) { - log := log.WithField("user", userID) - - log.Debug("Creating or loading user") - - creds, err := credStorer.Get(userID) - if err != nil { - notifyKeychainRepair(eventListener, err) - return nil, nil, errors.Wrap(err, "failed to load user credentials") - } - - return &User{ - log: log, - panicHandler: panicHandler, - listener: eventListener, - credStorer: credStorer, - storeFactory: storeFactory, - userID: userID, - creds: creds, - }, creds, nil -} - -// connect connects a user. This includes -// - providing it with an authorised API client -// - loading its credentials from the credentials store -// - loading and unlocking its PGP keys -// - loading its store. -func (u *User) connect(client pmapi.Client, creds *credentials.Credentials) error { - u.log.Info("Connecting user") - - // Connected users have an API client. - u.client = client - - u.client.AddAuthRefreshHandler(u.handleAuthRefresh) - - // Save the latest credentials for the user. - u.creds = creds - - // Connected users have unlocked keys. - if err := u.unlockIfNecessary(); err != nil { - return err - } - - // Connected users have a store. - if err := u.loadStore(); err != nil { //nolint:revive easier to read - return err - } - - // If the client is already unlocked, we can unlock the store cache as well. - if client.IsUnlocked() { - kr, err := client.GetUserKeyRing() - if err != nil { - return err - } - - if err := u.store.UnlockCache(kr); err != nil { - return err - } - - u.store.StartWatcher() - } - - u.UpdateSpace(nil) - - return nil -} - -func (u *User) loadStore() error { - // Logged-out user keeps store running to access offline data. - // Therefore it is necessary to close it before re-init. - if u.store != nil { - if err := u.store.Close(); err != nil { - log.WithError(err).Error("Not able to close store") - } - u.store = nil - } - - store, err := u.storeFactory.New(u) - if err != nil { - return errors.Wrap(err, "failed to create store") - } - - u.store = store - - return nil -} - -func (u *User) handleAuthRefresh(auth *pmapi.AuthRefresh) { - u.log.Debug("User received auth refresh update") - - if auth == nil { - if err := u.logout(); err != nil { - log.WithError(err). - WithField("userID", u.userID). - Error("User logout failed while watching API auths") - } - return - } - - creds, err := u.credStorer.UpdateToken(u.userID, auth.UID, auth.RefreshToken) - if err != nil { - notifyKeychainRepair(u.listener, err) - u.log.WithError(err).Error("Failed to update refresh token in credentials store") - return - } - - u.creds = creds -} - -// clearStore removes the database. -func (u *User) clearStore() error { - u.log.Trace("Clearing user store") - - if u.store != nil { - if err := u.store.Remove(); err != nil { - return errors.Wrap(err, "failed to remove store") - } - } else { - u.log.Warn("Store is not initialized: cleaning up store files manually") - if err := u.storeFactory.Remove(u.userID); err != nil { - return errors.Wrap(err, "failed to remove store manually") - } - } - return nil -} - -// closeStore just closes the store without deleting it. -func (u *User) closeStore() error { - u.log.Trace("Closing user store") - - if u.store != nil { - if err := u.store.Close(); err != nil { - return errors.Wrap(err, "failed to close store") - } - } - - return nil -} - -// ID returns the user's userID. -func (u *User) ID() string { - return u.userID -} - -// UsedBytes returns number of bytes used on server. -func (u *User) UsedBytes() int64 { - return u.usedBytes -} - -// TotalBytes returns number of bytes available on server. -func (u *User) TotalBytes() int64 { - return u.totalBytes -} - -// UpdateSpace will update TotalBytes and UsedBytes values from API user. If -// pointer is nill it will get fresh user from API. API user can come from -// update event which means it doesn't contain all data. Therefore only -// positive values will be updated. -func (u *User) UpdateSpace(apiUser *pmapi.User) { - // If missing get latest pmapi.User from API instead of using cached - // values from client.CurrentUser() - if apiUser == nil { - var err error - apiUser, err = u.GetClient().GetUser(pmapi.ContextWithoutRetry(context.Background())) - if err != nil { - u.log.WithError(err).Warning("Cannot update user space") - return - } - } - - if apiUser.UsedSpace != nil { - u.usedBytes = *apiUser.UsedSpace - } - if apiUser.MaxSpace != nil { - u.totalBytes = *apiUser.MaxSpace - } -} - -// Username returns the user's username as found in the user's credentials. -func (u *User) Username() string { - u.lock.RLock() - defer u.lock.RUnlock() - - return u.creds.Name -} - -// IsConnected returns whether user is logged in. -func (u *User) IsConnected() bool { - u.lock.RLock() - defer u.lock.RUnlock() - - return u.creds.IsConnected() -} - -func (u *User) GetClient() pmapi.Client { - if err := u.unlockIfNecessary(); err != nil { - u.log.WithError(err).Error("Failed to unlock user") - } - return u.client -} - -// unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked. -func (u *User) unlockIfNecessary() error { - if !u.creds.IsConnected() { - return nil - } - - if u.client.IsUnlocked() { - return nil - } - - // unlockIfNecessary is called with every access to underlying pmapi - // client. Unlock should only finish unlocking when connection is back up. - // That means it should try it fast enough and not retry if connection - // is still down. - err := u.client.Unlock(pmapi.ContextWithoutRetry(context.Background()), u.creds.MailboxPassword) - if err == nil { - return nil - } - - if pmapi.IsFailedAuth(err) || pmapi.IsFailedUnlock(err) { - if logoutErr := u.logout(); logoutErr != nil { - u.log.WithError(logoutErr).Warn("Could not logout user") - } - return errors.Wrap(err, "failed to unlock user") - } - - switch errors.Cause(err) { - case pmapi.ErrNoConnection, pmapi.ErrUpgradeApplication: - u.log.WithError(err).Warn("Skipping unlock for known reason") - default: - u.log.WithError(err).Error("Unknown unlock issue") - } - - return nil -} - -// IsCombinedAddressMode returns whether user is set in combined or split mode. -// Combined mode is the default mode and is what users typically need. -// Split mode is mostly for outlook as it cannot handle sending e-mails from an -// address other than the primary one. -func (u *User) IsCombinedAddressMode() bool { - if u.store != nil { - return u.store.IsCombinedMode() - } - - return u.creds.IsCombinedAddressMode -} - -// GetPrimaryAddress returns the user's original address (which is -// not necessarily the same as the primary address, because a primary address -// might be an alias and be in position one). -func (u *User) GetPrimaryAddress() string { - u.lock.RLock() - defer u.lock.RUnlock() - - return u.creds.EmailList()[0] -} - -// GetStoreAddresses returns all addresses used by the store (so in combined mode, -// that's just the original address, but in split mode, that's all active addresses). -func (u *User) GetStoreAddresses() []string { - u.lock.RLock() - defer u.lock.RUnlock() - - if u.IsCombinedAddressMode() { - return u.creds.EmailList()[:1] - } - - return u.creds.EmailList() -} - -// GetAddresses returns list of all addresses. -func (u *User) GetAddresses() []string { - u.lock.RLock() - defer u.lock.RUnlock() - - return u.creds.EmailList() -} - -// GetAddressID returns the API ID of the given address. -func (u *User) GetAddressID(address string) (id string, err error) { - u.lock.RLock() - defer u.lock.RUnlock() - - if u.store != nil { - address = strings.ToLower(address) - return u.store.GetAddressID(address) - } - - if u.client == nil { - return "", errors.New("bridge account is not fully connected to server") - } - - addresses := u.client.Addresses() - pmapiAddress := addresses.ByEmail(address) - if pmapiAddress != nil { - return pmapiAddress.ID, nil - } - return "", errors.New("address not found") -} - -// GetBridgePassword returns bridge password. This is not a password of the PM -// account, but generated password for local purposes to not use a PM account -// in the clients (such as Thunderbird). -func (u *User) GetBridgePassword() string { - u.lock.RLock() - defer u.lock.RUnlock() - - return u.creds.BridgePassword -} - -// CheckBridgeLogin checks whether the user is logged in and the bridge -// IMAP/SMTP password is correct. -func (u *User) CheckBridgeLogin(password string) error { - if isApplicationOutdated { - u.listener.Emit(events.UpgradeApplicationEvent, "") - return pmapi.ErrUpgradeApplication - } - - u.lock.RLock() - defer u.lock.RUnlock() - - if !u.creds.IsConnected() { - u.listener.Emit(events.LogoutEvent, u.userID) - return ErrLoggedOutUser - } - - return u.creds.CheckPassword(password) -} - -// UpdateUser updates user details from API and saves to the credentials. -func (u *User) UpdateUser(ctx context.Context) error { - u.lock.Lock() - defer u.lock.Unlock() - defer u.listener.Emit(events.UserRefreshEvent, u.userID) - - user, err := u.client.UpdateUser(ctx) - if err != nil { - return err - } - - if err := u.client.ReloadKeys(ctx, u.creds.MailboxPassword); err != nil { - return errors.Wrap(err, "failed to reload keys") - } - - creds, err := u.credStorer.UpdateEmails(u.userID, u.client.Addresses().ActiveEmails()) - if err != nil { - notifyKeychainRepair(u.listener, err) - return err - } - - u.creds = creds - - u.UpdateSpace(user) - - return nil -} - -// SwitchAddressMode changes mode from combined to split and vice versa. The mode to switch to is determined by the -// state of the user's credentials in the credentials store. See `IsCombinedAddressMode` for more details. -func (u *User) SwitchAddressMode() error { - u.log.Trace("Switching user address mode") - - u.lock.Lock() - defer u.lock.Unlock() - defer u.listener.Emit(events.UserRefreshEvent, u.userID) - - u.CloseAllConnections() - - if u.store == nil { - return errors.New("store is not initialised") - } - - newAddressModeState := !u.IsCombinedAddressMode() - - if err := u.store.UseCombinedMode(newAddressModeState); err != nil { - return errors.Wrap(err, "could not switch store address mode") - } - - if u.creds.IsCombinedAddressMode == newAddressModeState { - return nil - } - - creds, err := u.credStorer.SwitchAddressMode(u.userID) - if err != nil { - notifyKeychainRepair(u.listener, err) - return errors.Wrap(err, "could not switch credentials store address mode") - } - - u.creds = creds - - return nil -} - -// logout is the same as Logout, but for internal purposes (logged out from -// the server) which emits LogoutEvent to notify other parts of the app. -func (u *User) logout() error { - u.lock.Lock() - wasConnected := u.creds.IsConnected() - u.lock.Unlock() - - err := u.Logout() - - if wasConnected { - u.listener.Emit(events.LogoutEvent, u.userID) - } - - return err -} - -// Logout logs out the user from pmapi, the credentials store, the mail store, and tries to remove as much -// sensitive data as possible. -func (u *User) Logout() error { - u.lock.Lock() - defer u.lock.Unlock() - defer u.listener.Emit(events.UserRefreshEvent, u.userID) - - u.log.Debug("Logging out user") - - if !u.creds.IsConnected() { - return nil - } - - if u.client == nil { - u.log.Warn("Failed to delete auth: no client") - } else if err := u.client.AuthDelete(context.Background()); err != nil { - u.log.WithError(err).Warn("Failed to delete auth") - } - - creds, err := u.credStorer.Logout(u.userID) - if err != nil { - notifyKeychainRepair(u.listener, err) - u.log.WithError(err).Warn("Could not log user out from credentials store") - - if err := u.credStorer.Delete(u.userID); err != nil { - notifyKeychainRepair(u.listener, err) - u.log.WithError(err).Error("Could not delete user from credentials store") - } - } else { - u.creds = creds - } - - // Do not close whole store, just event loop. Some information might be needed offline (e.g. addressID) - u.closeEventLoopAndCacher() - - u.CloseAllConnections() - - runtime.GC() - - return nil -} - -func (u *User) closeEventLoopAndCacher() { - if u.store == nil { - return - } - - u.store.CloseEventLoopAndCacher() -} - -// CloseAllConnections calls CloseConnection for all users addresses. -func (u *User) CloseAllConnections() { - for _, address := range u.creds.EmailList() { - u.CloseConnection(address) - } - - if u.store != nil { - u.store.SetChangeNotifier(nil) - } -} - -// CloseConnection emits closeConnection event on `address` which should close all active connection. -func (u *User) CloseConnection(address string) { - u.listener.Emit(events.CloseConnectionEvent, address) -} - -func (u *User) GetStore() *store.Store { - return u.store -} diff --git a/internal/users/user_credentials_test.go b/internal/users/user_credentials_test.go deleted file mode 100644 index 5b70fd13..00000000 --- a/internal/users/user_credentials_test.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "context" - "testing" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - gomock "github.com/golang/mock/gomock" - "github.com/pkg/errors" - r "github.com/stretchr/testify/require" -) - -func TestUpdateUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - gomock.InOrder( - m.pmapiClient.EXPECT().UpdateUser(gomock.Any()).Return(testPMAPIUser, nil), - m.pmapiClient.EXPECT().ReloadKeys(gomock.Any(), testCredentials.MailboxPassword).Return(nil), - m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), - - m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}).Return(testCredentials, nil), - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), - ) - - r.NoError(t, user.UpdateUser(context.Background())) -} - -func TestUserSwitchAddressMode(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - // Ignore any sync on background. - m.pmapiClient.EXPECT().ListMessages(gomock.Any(), gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes() - - // Check initial state. - r.True(t, user.store.IsCombinedMode()) - r.True(t, user.creds.IsCombinedAddressMode) - r.True(t, user.IsCombinedAddressMode()) - - // Mock change to split mode. - gomock.InOrder( - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), - m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return([]*pmapi.Label{}, nil), - m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil), - m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), - m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentialsSplit, nil), - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), - ) - - // Check switch to split mode. - r.NoError(t, user.SwitchAddressMode()) - r.False(t, user.store.IsCombinedMode()) - r.False(t, user.creds.IsCombinedAddressMode) - r.False(t, user.IsCombinedAddressMode()) - - // Mock change to combined mode. - gomock.InOrder( - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me"), - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me"), - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "alsouser@pm.me"), - m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return([]*pmapi.Label{}, nil), - m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil), - m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), - m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentials, nil), - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), - ) - - // Check switch to combined mode. - r.NoError(t, user.SwitchAddressMode()) - r.True(t, user.store.IsCombinedMode()) - r.True(t, user.creds.IsCombinedAddressMode) - r.True(t, user.IsCombinedAddressMode()) -} - -func TestLogoutUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - gomock.InOrder( - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), - m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), - ) - - err := user.Logout() - r.NoError(t, err) -} - -func TestLogoutUserFailsLogout(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - gomock.InOrder( - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), - m.credentialsStore.EXPECT().Logout("user").Return(nil, errors.New("logout failed")), - m.credentialsStore.EXPECT().Delete("user").Return(nil), - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), - ) - - err := user.Logout() - r.NoError(t, err) -} - -func TestCheckBridgeLogin(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - err := user.CheckBridgeLogin(testCredentials.BridgePassword) - r.NoError(t, err) -} - -func TestCheckBridgeLoginUpgradeApplication(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "") - - isApplicationOutdated = true - - err := user.CheckBridgeLogin("any-pass") - r.Equal(t, pmapi.ErrUpgradeApplication, err) - - isApplicationOutdated = false -} - -func TestCheckBridgeLoginLoggedOut(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - gomock.InOrder( - // Mock init of user. - m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil), - m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()), - m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return(nil, pmapi.ErrUnauthorized), - m.pmapiClient.EXPECT().Addresses().Return(nil), - - // Mock CheckBridgeLogin. - m.eventListener.EXPECT().Emit(events.LogoutEvent, "user"), - ) - - user, _, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.storeMaker) - r.NoError(t, err) - - err = user.connect(m.pmapiClient, testCredentialsDisconnected) - r.Error(t, err) - defer cleanUpUserData(user) - - err = user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword) - r.Equal(t, ErrLoggedOutUser, err) -} - -func TestCheckBridgeLoginBadPassword(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - err := user.CheckBridgeLogin("wrong!") - r.EqualError(t, err, "backend/credentials: incorrect password") -} diff --git a/internal/users/user_new_test.go b/internal/users/user_new_test.go deleted file mode 100644 index 8f203790..00000000 --- a/internal/users/user_new_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "errors" - "testing" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/users/credentials" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - gomock "github.com/golang/mock/gomock" - r "github.com/stretchr/testify/require" -) - -func TestNewUserNoCredentialsStore(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("fail")) - - _, _, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.storeMaker) - r.Error(t, err) -} - -func TestNewUserUnlockFails(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - gomock.InOrder( - // Init of user. - m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), - m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()), - m.pmapiClient.EXPECT().IsUnlocked().Return(false), - m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(pmapi.ErrUnlockFailed{OriginalError: errors.New("bad password")}), - - // Handle of unlock error. - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), - m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), - m.eventListener.EXPECT().Emit(events.LogoutEvent, "user"), - ) - - checkNewUserHasCredentials(m, "failed to unlock user: bad password", testCredentialsDisconnected) -} - -func TestNewUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil) - mockInitConnectedUser(t, m) - mockEventLoopNoAction(m) - - checkNewUserHasCredentials(m, "", testCredentials) -} - -func checkNewUserHasCredentials(m mocks, wantErr string, wantCreds *credentials.Credentials) { - user, _, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.storeMaker) - r.NoError(m.t, err) - defer cleanUpUserData(user) - - err = user.connect(m.pmapiClient, testCredentials) - if wantErr == "" { - r.NoError(m.t, err) - } else { - r.EqualError(m.t, err, wantErr) - } - - r.Equal(m.t, wantCreds, user.creds) - - waitForEvents() -} diff --git a/internal/users/user_store_test.go b/internal/users/user_store_test.go deleted file mode 100644 index 50cc68be..00000000 --- a/internal/users/user_store_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "testing" - - r "github.com/stretchr/testify/require" -) - -func _TestNeverLongStorePath(t *testing.T) { //nolint:unused,deadcode - r.Fail(t, "not implemented") -} - -func TestClearStoreWithStore(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - r.Nil(t, user.store.Close()) - user.store = nil - r.Nil(t, user.clearStore()) -} - -func TestClearStoreWithoutStore(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - user := testNewUser(t, m) - defer cleanUpUserData(user) - - r.NotNil(t, user.store) - r.Nil(t, user.clearStore()) -} diff --git a/internal/users/user_test.go b/internal/users/user_test.go deleted file mode 100644 index c90016f2..00000000 --- a/internal/users/user_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "testing" - - r "github.com/stretchr/testify/require" -) - -// testNewUser sets up a new, authorised user. -func testNewUser(t *testing.T, m mocks) *User { - m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil) - mockInitConnectedUser(t, m) - mockEventLoopNoAction(m) - - user, creds, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.storeMaker) - r.NoError(m.t, err) - - err = user.connect(m.pmapiClient, creds) - r.NoError(m.t, err) - - return user -} - -func cleanUpUserData(u *User) { - _ = u.clearStore() -} diff --git a/internal/users/users.go b/internal/users/users.go deleted file mode 100644 index 09ffb894..00000000 --- a/internal/users/users.go +++ /dev/null @@ -1,547 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package users provides core business logic providing API over credentials store and PM API. -package users - -import ( - "context" - "strings" - "sync" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/metrics" - "github.com/ProtonMail/proton-bridge/v2/internal/users/credentials" - "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" - "github.com/ProtonMail/proton-bridge/v2/pkg/listener" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/bradenaw/juniper/xslices" - "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" - logrus "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" -) - -var ( - log = logrus.WithField("pkg", "users") //nolint:gochecknoglobals - isApplicationOutdated = false //nolint:gochecknoglobals - - // ErrWrongMailboxPassword is returned when login password is OK but - // not the mailbox one. - ErrWrongMailboxPassword = errors.New("wrong mailbox password") - - // ErrUserAlreadyConnected is returned when authentication was OK but - // there is already active account for this user. - ErrUserAlreadyConnected = errors.New("user is already connected") -) - -// Users is a struct handling users. -type Users struct { - locations Locator - panicHandler PanicHandler - events listener.Listener - clientManager pmapi.Manager - credStorer CredentialsStorer - storeFactory StoreMaker - - // users is a list of accounts that have been added to the app. - // They are stored sorted in the credentials store in the order - // that they were added to the app chronologically. - // People are used to that and so we preserve that ordering here. - users []*User - - lock sync.RWMutex -} - -func New( - locations Locator, - panicHandler PanicHandler, - eventListener listener.Listener, - clientManager pmapi.Manager, - credStorer CredentialsStorer, - storeFactory StoreMaker, -) *Users { - log.Trace("Creating new users") - - u := &Users{ - locations: locations, - panicHandler: panicHandler, - events: eventListener, - clientManager: clientManager, - credStorer: credStorer, - storeFactory: storeFactory, - lock: sync.RWMutex{}, - } - - go func() { - defer panicHandler.HandlePanic() - u.watchEvents() - }() - - if u.credStorer == nil { - log.Error("No credentials store is available") - } else if err := u.loadUsersFromCredentialsStore(); err != nil { - log.WithError(err).Error("Could not load all users from credentials store") - } - - return u -} - -func (u *Users) watchEvents() { - upgradeCh := u.events.ProvideChannel(events.UpgradeApplicationEvent) - internetConnChangedCh := u.events.ProvideChannel(events.InternetConnChangedEvent) - - for { - select { - case <-upgradeCh: - isApplicationOutdated = true - u.closeAllConnections() - case stat := <-internetConnChangedCh: - if stat != events.InternetOn { - continue - } - for _, user := range u.users { - if user.store == nil { - if err := user.loadStore(); err != nil { - log.WithError(err).Error("Failed to load store after reconnecting") - } - } - - if user.totalBytes == 0 { - user.UpdateSpace(nil) - } - } - } - } -} - -func (u *Users) loadUsersFromCredentialsStore() error { - u.lock.Lock() - defer u.lock.Unlock() - - userIDs, err := u.credStorer.List() - if err != nil { - notifyKeychainRepair(u.events, err) - return err - } - - for _, userID := range userIDs { - l := log.WithField("user", userID) - user, creds, err := newUser(u.panicHandler, userID, u.events, u.credStorer, u.storeFactory) - if err != nil { - l.WithError(err).Warn("Could not create user, skipping") - continue - } - - u.users = append(u.users, user) - - if creds.IsConnected() { - // If there is no connection, we don't want to retry. Load should - // happen fast enough to not block GUI. When connection is back up, - // watchEvents and unlockIfNecessary will finish user init later. - if err := u.loadConnectedUser(pmapi.ContextWithoutRetry(context.Background()), user, creds); err != nil { - l.WithError(err).Warn("Could not load connected user") - } - } else { - l.Warn("User is disconnected and must be connected manually") - if err := user.connect(u.clientManager.NewClient("", "", "", time.Time{}), creds); err != nil { - l.WithError(err).Warn("Could not load disconnected user") - } - } - } - - return err -} - -func (u *Users) loadConnectedUser(ctx context.Context, user *User, creds *credentials.Credentials) error { - uid, ref, err := creds.SplitAPIToken() - if err != nil { - return errors.Wrap(err, "could not get user's refresh token") - } - - client, auth, err := u.clientManager.NewClientWithRefresh(ctx, uid, ref) - if err != nil { - // When client cannot be refreshed right away due to no connection, - // we create client which will refresh automatically when possible. - connectErr := user.connect(u.clientManager.NewClient(uid, "", ref, time.Time{}), creds) - - switch errors.Cause(err) { - case pmapi.ErrNoConnection, pmapi.ErrUpgradeApplication: - return connectErr - } - - if pmapi.IsFailedAuth(connectErr) { - if logoutErr := user.logout(); logoutErr != nil { - logrus.WithError(logoutErr).Warn("Could not logout user") - } - } - return errors.Wrap(err, "could not refresh token") - } - - // Update the user's credentials with the latest auth used to connect this user. - if creds, err = u.credStorer.UpdateToken(creds.UserID, auth.UID, auth.RefreshToken); err != nil { - notifyKeychainRepair(u.events, err) - return errors.Wrap(err, "could not create get user's refresh token") - } - - return user.connect(client, creds) -} - -func (u *Users) closeAllConnections() { - for _, user := range u.users { - user.CloseAllConnections() - } -} - -// Login authenticates a user by username/password, returning an authorised client and an auth object. -// The authorisation scope may not yet be full if the user has 2FA enabled. -func (u *Users) Login(username string, password []byte) (authClient pmapi.Client, auth *pmapi.Auth, err error) { - u.crashBandicoot(username) - - return u.clientManager.NewClientWithLogin(context.Background(), username, password) -} - -// FinishLogin finishes the login procedure and adds the user into the credentials store. -func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []byte) (userID string, err error) { //nolint:funlen - apiUser, passphrase, err := getAPIUser(context.Background(), client, password) - if err != nil { - return "", err - } - - if user, ok := u.hasUser(apiUser.ID); ok { - if user.IsConnected() { - if err := client.AuthDelete(context.Background()); err != nil { - logrus.WithError(err).Warn("Failed to delete new auth session") - } - - return user.ID(), ErrUserAlreadyConnected - } - - // Update the user's credentials with the latest auth used to connect this user. - if _, err := u.credStorer.UpdateToken(auth.UserID, auth.UID, auth.RefreshToken); err != nil { - notifyKeychainRepair(u.events, err) - return "", errors.Wrap(err, "failed to load user credentials") - } - - // Update the password in case the user changed it. - creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase) - if err != nil { - notifyKeychainRepair(u.events, err) - return "", errors.Wrap(err, "failed to update password of user in credentials store") - } - - // will go and unlock cache if not already done - if err := user.connect(client, creds); err != nil { - return "", errors.Wrap(err, "failed to reconnect existing user") - } - - u.events.Emit(events.UserRefreshEvent, apiUser.ID) - - return user.ID(), nil - } - - if err := u.addNewUser(client, apiUser, auth, passphrase); err != nil { - return "", errors.Wrap(err, "failed to add new user") - } - - u.events.Emit(events.UserRefreshEvent, apiUser.ID) - - return apiUser.ID, nil -} - -// addNewUser adds a new user. -func (u *Users) addNewUser(client pmapi.Client, apiUser *pmapi.User, auth *pmapi.Auth, passphrase []byte) error { - u.lock.Lock() - defer u.lock.Unlock() - - if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, passphrase, client.Addresses().ActiveEmails()); err != nil { - notifyKeychainRepair(u.events, err) - return errors.Wrap(err, "failed to add user credentials to credentials store") - } - - user, creds, err := newUser(u.panicHandler, apiUser.ID, u.events, u.credStorer, u.storeFactory) - if err != nil { - return errors.Wrap(err, "failed to create new user") - } - - if err := user.connect(client, creds); err != nil { - return errors.Wrap(err, "failed to connect new user") - } - - if err := u.SendMetric(metrics.New(metrics.Setup, metrics.NewUser, metrics.NoLabel)); err != nil { - log.WithError(err).Error("Failed to send metric") - } - - u.users = append(u.users, user) - - return nil -} - -func getAPIUser(ctx context.Context, client pmapi.Client, password []byte) (*pmapi.User, []byte, error) { - salt, err := client.AuthSalt(ctx) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to get salt") - } - - passphrase, err := pmapi.HashMailboxPassword(password, salt) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to hash password") - } - - // We unlock the user's PGP key here to detect if the user's mailbox password is wrong. - if err := client.Unlock(ctx, passphrase); err != nil { - return nil, nil, ErrWrongMailboxPassword - } - - user, err := client.CurrentUser(ctx) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to load user data") - } - - return user, passphrase, nil -} - -// GetUsers returns all added users into keychain (even logged out users). -func (u *Users) GetUsers() []*User { - u.lock.RLock() - defer u.lock.RUnlock() - - return u.users -} - -// GetUserIDs returns IDs of all added users into keychain (even logged out users). -func (u *Users) GetUserIDs() []string { - u.lock.RLock() - defer u.lock.RUnlock() - - return xslices.Map(u.users, func(user *User) string { - return user.ID() - }) -} - -// GetUser returns a user by `query` which is compared to users' ID, username or any attached e-mail address. -func (u *Users) GetUser(query string) (*User, error) { - u.crashBandicoot(query) - - u.lock.RLock() - defer u.lock.RUnlock() - - for _, user := range u.users { - if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) { - return user, nil - } - for _, address := range user.GetAddresses() { - if strings.EqualFold(address, query) { - return user, nil - } - } - } - - return nil, errors.New("user " + query + " not found") -} - -// GetUserInfo returns user about the user with the given ID. -func (u *Users) GetUserInfo(userID string) (UserInfo, error) { - u.lock.RLock() - defer u.lock.RUnlock() - - idx := slices.IndexFunc(u.users, func(user *User) bool { - return user.userID == userID - }) - if idx < 0 { - return UserInfo{}, errors.New("no such user") - } - - user := u.users[idx] - - var mode AddressMode - - if user.IsCombinedAddressMode() { - mode = CombinedMode - } else { - mode = SplitMode - } - - return UserInfo{ - ID: userID, - Username: user.Username(), - Password: user.GetBridgePassword(), - - Addresses: user.GetAddresses(), - Primary: slices.Index(user.GetAddresses(), user.GetPrimaryAddress()), - - UsedBytes: user.UsedBytes(), - TotalBytes: user.TotalBytes(), - - Connected: user.IsConnected(), - Mode: mode, - }, nil -} - -// ClearData closes all connections (to release db files and so on) and clears all data. -func (u *Users) ClearData() error { - var result error - - for _, user := range u.users { - if err := user.Logout(); err != nil { - result = multierror.Append(result, err) - } - - if err := user.closeStore(); err != nil { - result = multierror.Append(result, err) - } - } - - if err := u.locations.Clear(); err != nil { - result = multierror.Append(result, err) - } - - return result -} - -func (u *Users) LogoutUser(userID string) error { - u.lock.RLock() - defer u.lock.RUnlock() - - idx := slices.IndexFunc(u.users, func(user *User) bool { - return user.userID == userID - }) - if idx < 0 { - return errors.New("no such user") - } - - return u.users[idx].Logout() -} - -func (u *Users) SetAddressMode(userID string, mode AddressMode) error { - u.lock.RLock() - defer u.lock.RUnlock() - - idx := slices.IndexFunc(u.users, func(user *User) bool { - return user.userID == userID - }) - if idx < 0 { - return errors.New("no such user") - } - - if mode == CombinedMode && u.users[idx].IsCombinedAddressMode() { - return nil - } - - if mode == SplitMode && !u.users[idx].IsCombinedAddressMode() { - return nil - } - - return u.users[idx].SwitchAddressMode() -} - -// DeleteUser deletes user completely; it logs user out from the API, stops any -// active connection, deletes from credentials store and removes from the Bridge struct. -func (u *Users) DeleteUser(userID string, clearStore bool) error { - u.lock.Lock() - defer u.lock.Unlock() - defer u.events.Emit(events.UserRefreshEvent, userID) - - log := log.WithField("user", userID) - - for idx, user := range u.users { - if user.ID() == userID { - if err := user.Logout(); err != nil { - log.WithError(err).Error("Cannot logout user") - // We can try to continue to remove the user. - // Token will still be valid, but will expire eventually. - } - - if err := user.closeStore(); err != nil { - log.WithError(err).Error("Failed to close user store") - } - - if clearStore { - // Clear cache after closing connections (done in logout). - if err := user.clearStore(); err != nil { - log.WithError(err).Error("Failed to clear user") - } - } - - if err := u.credStorer.Delete(userID); err != nil { - notifyKeychainRepair(u.events, err) - log.WithError(err).Error("Cannot remove user") - return err - } - u.users = append(u.users[:idx], u.users[idx+1:]...) - return nil - } - } - - return errors.New("user " + userID + " not found") -} - -// ClearUsers deletes all users. -func (u *Users) ClearUsers() error { - var result error - - for _, user := range u.GetUsers() { - if err := u.DeleteUser(user.ID(), false); err != nil { - result = multierror.Append(result, err) - } - } - - return result -} - -// SendMetric sends a metric. We don't want to return any errors, only log them. -func (u *Users) SendMetric(m metrics.Metric) error { - cat, act, lab := m.Get() - - if err := u.clientManager.SendSimpleMetric(context.Background(), string(cat), string(act), string(lab)); err != nil { - return err - } - - log.WithFields(logrus.Fields{ - "cat": cat, - "act": act, - "lab": lab, - }).Debug("Metric successfully sent") - - return nil -} - -// hasUser returns whether the struct currently has a user with ID `id`. -func (u *Users) hasUser(id string) (user *User, ok bool) { - for _, u := range u.users { - if u.ID() == id { - user, ok = u, true - return - } - } - - return -} - -// "Easter egg" for testing purposes. -func (u *Users) crashBandicoot(username string) { - if username == "crash@bandicoot" { - panic("Your wish is my command… I crash!") - } -} - -func notifyKeychainRepair(l listener.Listener, err error) { - if err == keychain.ErrMacKeychainRebuild { - l.Emit(events.CredentialsErrorEvent, err.Error()) - } -} diff --git a/internal/users/users_clear_test.go b/internal/users/users_clear_test.go deleted file mode 100644 index b74a2988..00000000 --- a/internal/users/users_clear_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "testing" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - gomock "github.com/golang/mock/gomock" - r "github.com/stretchr/testify/require" -) - -func TestClearData(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - users := testNewUsersWithUsers(t, m) - defer cleanUpUsersData(users) - - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "users") - - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me") - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me") - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "alsouser@pm.me") - - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()) - m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil) - - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()) - m.credentialsStore.EXPECT().Logout("users").Return(testCredentialsSplitDisconnected, nil) - - m.locator.EXPECT().Clear() - - r.NoError(t, users.ClearData()) -} diff --git a/internal/users/users_delete_test.go b/internal/users/users_delete_test.go deleted file mode 100644 index bce1ac7c..00000000 --- a/internal/users/users_delete_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "errors" - "testing" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - gomock "github.com/golang/mock/gomock" - r "github.com/stretchr/testify/require" -) - -func TestDeleteUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - users := testNewUsersWithUsers(t, m) - defer cleanUpUsersData(users) - - gomock.InOrder( - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), - m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), - m.credentialsStore.EXPECT().Delete("user").Return(nil), - ) - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") - - err := users.DeleteUser("user", true) - r.NoError(t, err) - r.Equal(t, 1, len(users.users)) -} - -// Even when logout fails, delete is done. -func TestDeleteUserWithFailingLogout(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - users := testNewUsersWithUsers(t, m) - defer cleanUpUsersData(users) - - gomock.InOrder( - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), - m.credentialsStore.EXPECT().Logout("user").Return(nil, errors.New("logout failed")), - // Once called from user.Logout after failed creds.Logout as fallback, and once at the end of users.Logout. - m.credentialsStore.EXPECT().Delete("user").Return(nil), - m.credentialsStore.EXPECT().Delete("user").Return(nil), - ) - - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") - - err := users.DeleteUser("user", true) - r.NoError(t, err) - r.Equal(t, 1, len(users.users)) -} diff --git a/internal/users/users_get_test.go b/internal/users/users_get_test.go deleted file mode 100644 index 21746c8f..00000000 --- a/internal/users/users_get_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "testing" - - r "github.com/stretchr/testify/require" -) - -func TestGetNoUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - checkUsersGetUser(t, m, "nouser", -1, "user nouser not found") -} - -func TestGetUserByID(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - checkUsersGetUser(t, m, "user", 0, "") - checkUsersGetUser(t, m, "users", 1, "") -} - -func TestGetUserByName(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - checkUsersGetUser(t, m, "username", 0, "") - checkUsersGetUser(t, m, "usersname", 1, "") -} - -func TestGetUserByEmail(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - checkUsersGetUser(t, m, "user@pm.me", 0, "") - checkUsersGetUser(t, m, "users@pm.me", 1, "") - checkUsersGetUser(t, m, "anotheruser@pm.me", 1, "") - checkUsersGetUser(t, m, "alsouser@pm.me", 1, "") -} - -func checkUsersGetUser(t *testing.T, m mocks, query string, index int, expectedError string) { - users := testNewUsersWithUsers(t, m) - defer cleanUpUsersData(users) - - user, err := users.GetUser(query) - - if expectedError != "" { - r.EqualError(m.t, err, expectedError) - } else { - r.NoError(m.t, err) - } - - var expectedUser *User - if index >= 0 { - expectedUser = users.users[index] - } - r.Equal(m.t, expectedUser, user) -} diff --git a/internal/users/users_login_test.go b/internal/users/users_login_test.go deleted file mode 100644 index 502180c2..00000000 --- a/internal/users/users_login_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "testing" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/metrics" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - gomock "github.com/golang/mock/gomock" - "github.com/pkg/errors" - r "github.com/stretchr/testify/require" -) - -func TestUsersFinishLoginBadMailboxPassword(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - // Init users with no user from keychain. - m.credentialsStore.EXPECT().List().Return([]string{}, nil) - - // Set up mocks for FinishLogin. - m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil) - m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("no keys could be unlocked")) - - checkUsersFinishLogin(t, m, testAuthRefresh, testCredentials.MailboxPassword, "", ErrWrongMailboxPassword) -} - -func TestUsersFinishLoginNewUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - // Init users with no user from keychain. - m.credentialsStore.EXPECT().List().Return([]string{}, nil) - - mockAddingConnectedUser(t, m) - mockEventLoopNoAction(m) - - m.clientManager.EXPECT().SendSimpleMetric(gomock.Any(), string(metrics.Setup), string(metrics.NewUser), string(metrics.NoLabel)) - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, testCredentials.UserID) - - checkUsersFinishLogin(t, m, testAuthRefresh, testCredentials.MailboxPassword, testCredentials.UserID, nil) -} - -func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - // Mock loading disconnected user. - m.credentialsStore.EXPECT().List().Return([]string{testCredentialsDisconnected.UserID}, nil) - mockLoadingDisconnectedUser(m, testCredentialsDisconnected) - - // Mock process of FinishLogin of already added user. - gomock.InOrder( - m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil), - m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil), - m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUserDisconnected, nil), - m.credentialsStore.EXPECT().UpdateToken(testCredentialsDisconnected.UserID, testAuthRefresh.UID, testAuthRefresh.RefreshToken).Return(testCredentials, nil), - m.credentialsStore.EXPECT().UpdatePassword(testCredentialsDisconnected.UserID, testCredentials.MailboxPassword).Return(testCredentials, nil), - ) - mockInitConnectedUser(t, m) - mockEventLoopNoAction(m) - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, testCredentialsDisconnected.UserID) - - authRefresh := &pmapi.Auth{ - UserID: testCredentialsDisconnected.UserID, - AuthRefresh: pmapi.AuthRefresh{ - UID: "uid", - AccessToken: "acc", - RefreshToken: "ref", - }, - } - checkUsersFinishLogin(t, m, authRefresh, testCredentials.MailboxPassword, testCredentialsDisconnected.UserID, nil) -} - -func TestUsersFinishLoginConnectedUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - // Mock loading connected user. - m.credentialsStore.EXPECT().List().Return([]string{testCredentials.UserID}, nil) - mockLoadingConnectedUser(t, m, testCredentials) - mockEventLoopNoAction(m) - - // Mock process of FinishLogin of already connected user. - gomock.InOrder( - m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil), - m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil), - m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUser, nil), - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), - ) - - users := testNewUsers(t, m) - defer cleanUpUsersData(users) - - _, err := users.FinishLogin(m.pmapiClient, testAuthRefresh, testCredentials.MailboxPassword) - r.EqualError(t, err, "user is already connected") -} - -func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword []byte, expectedUserID string, expectedErr error) { - users := testNewUsers(t, m) - defer cleanUpUsersData(users) - - userID, err := users.FinishLogin(m.pmapiClient, auth, mailboxPassword) - - r.Equal(t, expectedErr, err) - - if expectedUserID != "" { - r.Equal(t, expectedUserID, userID) - r.Equal(t, 1, len(users.users)) - r.Equal(t, expectedUserID, users.users[0].ID()) - } else { - r.Equal(t, "", userID) - r.Equal(t, 0, len(users.users)) - } -} diff --git a/internal/users/users_new_test.go b/internal/users/users_new_test.go deleted file mode 100644 index 9c93de9b..00000000 --- a/internal/users/users_new_test.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "errors" - "testing" - time "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/users/credentials" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - gomock "github.com/golang/mock/gomock" - r "github.com/stretchr/testify/require" -) - -func TestNewUsersNoKeychain(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain")) - checkUsersNew(t, m, []*credentials.Credentials{}) -} - -func TestNewUsersWithoutUsersInCredentialsStore(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.credentialsStore.EXPECT().List().Return([]string{}, nil) - checkUsersNew(t, m, []*credentials.Credentials{}) -} - -func TestNewUsersWithConnectedUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.credentialsStore.EXPECT().List().Return([]string{testCredentials.UserID}, nil) - mockLoadingConnectedUser(t, m, testCredentials) - mockEventLoopNoAction(m) - checkUsersNew(t, m, []*credentials.Credentials{testCredentials}) -} - -func TestNewUsersWithDisconnectedUser(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.credentialsStore.EXPECT().List().Return([]string{testCredentialsDisconnected.UserID}, nil) - mockLoadingDisconnectedUser(m, testCredentialsDisconnected) - checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected}) -} - -// Tests two users with different states and checks also the order from -// credentials store is kept also in array of users. -func TestNewUsersWithUsers(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.credentialsStore.EXPECT().List().Return([]string{testCredentialsDisconnected.UserID, testCredentials.UserID}, nil) - mockLoadingDisconnectedUser(m, testCredentialsDisconnected) - mockLoadingConnectedUser(t, m, testCredentials) - mockEventLoopNoAction(m) - checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials}) -} - -func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) { - m := initMocks(t) - defer m.ctrl.Finish() - - m.clientManager.EXPECT().NewClientWithRefresh(gomock.Any(), "uid", "acc").Return(nil, nil, pmapi.ErrAuthFailed{OriginalError: errors.New("bad token")}) - m.clientManager.EXPECT().NewClient("uid", "", "acc", time.Time{}).Return(m.pmapiClient) - m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()) - m.pmapiClient.EXPECT().IsUnlocked().Return(false) - m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(pmapi.ErrAuthFailed{OriginalError: errors.New("not authorized")}) - m.pmapiClient.EXPECT().AuthDelete(gomock.Any()) - - m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil) - m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil) - m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil) - - m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") - m.eventListener.EXPECT().Emit(events.LogoutEvent, "user") - m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") - - checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected}) -} - -func checkUsersNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) { - users := testNewUsers(t, m) - defer cleanUpUsersData(users) - - r.Equal(m.t, len(expectedCredentials), len(users.GetUsers())) - - credentials := []*credentials.Credentials{} - for _, user := range users.users { - credentials = append(credentials, user.creds) - } - - r.Equal(m.t, expectedCredentials, credentials) -} diff --git a/internal/users/users_test.go b/internal/users/users_test.go deleted file mode 100644 index ed3dfd2b..00000000 --- a/internal/users/users_test.go +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package users - -import ( - "fmt" - "os" - "runtime" - "runtime/debug" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/sentry" - "github.com/ProtonMail/proton-bridge/v2/internal/store" - "github.com/ProtonMail/proton-bridge/v2/internal/store/cache" - "github.com/ProtonMail/proton-bridge/v2/internal/users/credentials" - usersmocks "github.com/ProtonMail/proton-bridge/v2/internal/users/mocks" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - pmapimocks "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi/mocks" - tests "github.com/ProtonMail/proton-bridge/v2/test" - gomock "github.com/golang/mock/gomock" - "github.com/sirupsen/logrus" - r "github.com/stretchr/testify/require" -) - -func TestMain(m *testing.M) { - if os.Getenv("VERBOSITY") == "fatal" { - logrus.SetLevel(logrus.FatalLevel) - } - - if os.Getenv("VERBOSITY") == "trace" { - logrus.SetLevel(logrus.TraceLevel) - } - - os.Exit(m.Run()) -} - -var ( - testAuthRefresh = &pmapi.Auth{ //nolint:gochecknoglobals - UserID: "user", - AuthRefresh: pmapi.AuthRefresh{ - UID: "uid", - AccessToken: "acc", - RefreshToken: "ref", - }, - } - - testCredentials = &credentials.Credentials{ //nolint:gochecknoglobals - UserID: "user", - Name: "username", - Emails: "user@pm.me", - APIToken: "uid:acc", - MailboxPassword: []byte("pass"), - BridgePassword: "0123456789abcdef", - Version: "v1", - Timestamp: 123456789, - IsHidden: false, - IsCombinedAddressMode: true, - } - - testCredentialsSplit = &credentials.Credentials{ //nolint:gochecknoglobals - UserID: "users", - Name: "usersname", - Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me", - APIToken: "uid:acc", - MailboxPassword: []byte("pass"), - BridgePassword: "0123456789abcdef", - Version: "v1", - Timestamp: 123456789, - IsHidden: false, - IsCombinedAddressMode: false, - } - - testCredentialsDisconnected = &credentials.Credentials{ //nolint:gochecknoglobals - UserID: "userDisconnected", - Name: "username", - Emails: "user@pm.me", - APIToken: "", - MailboxPassword: []byte{}, - BridgePassword: "0123456789abcdef", - Version: "v1", - Timestamp: 123456789, - IsHidden: false, - IsCombinedAddressMode: true, - } - - testCredentialsSplitDisconnected = &credentials.Credentials{ //nolint:gochecknoglobals - UserID: "usersDisconnected", - Name: "usersname", - Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me", - APIToken: "", - MailboxPassword: []byte{}, - BridgePassword: "0123456789abcdef", - Version: "v1", - Timestamp: 123456789, - IsHidden: false, - IsCombinedAddressMode: false, - } - - usedSpace = int64(1048576) - maxSpace = int64(10485760) - - testPMAPIUser = &pmapi.User{ //nolint:gochecknoglobals - ID: "user", - Name: "username", - UsedSpace: &usedSpace, - MaxSpace: &maxSpace, - } - - testPMAPIUserDisconnected = &pmapi.User{ //nolint:gochecknoglobals - ID: "userDisconnected", - Name: "username", - } - - testPMAPIAddress = &pmapi.Address{ //nolint:gochecknoglobals - ID: "testAddressID", - Type: pmapi.OriginalAddress, - Email: "user@pm.me", - Receive: true, - } - - testPMAPIEvent = &pmapi.Event{ // nolint:gochecknoglobals - EventID: "ACXDmTaBub14w==", - } -) - -type mocks struct { - t *testing.T - - ctrl *gomock.Controller - locator *usersmocks.MockLocator - PanicHandler *usersmocks.MockPanicHandler - credentialsStore *usersmocks.MockCredentialsStorer - storeMaker *usersmocks.MockStoreMaker - eventListener *usersmocks.MockListener - - clientManager *pmapimocks.MockManager - pmapiClient *pmapimocks.MockClient - - storeCache *store.Events -} - -func initMocks(t *testing.T) mocks { - var mockCtrl *gomock.Controller - if os.Getenv("VERBOSITY") == "trace" { - mockCtrl = gomock.NewController(&fullStackReporter{t}) - } else { - mockCtrl = gomock.NewController(t) - } - - cacheFile, err := os.CreateTemp("", "bridge-store-cache-*.db") - r.NoError(t, err, "could not get temporary file for store cache") - r.NoError(t, cacheFile.Close()) - - m := mocks{ - t: t, - - ctrl: mockCtrl, - locator: usersmocks.NewMockLocator(mockCtrl), - PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl), - credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl), - storeMaker: usersmocks.NewMockStoreMaker(mockCtrl), - eventListener: usersmocks.NewMockListener(mockCtrl), - - clientManager: pmapimocks.NewMockManager(mockCtrl), - pmapiClient: pmapimocks.NewMockClient(mockCtrl), - - storeCache: store.NewEvents(cacheFile.Name()), - } - - // Called during clean-up. - m.PanicHandler.EXPECT().HandlePanic().AnyTimes() - - // Set up store factory. - m.storeMaker.EXPECT().New(gomock.Any()).DoAndReturn(func(user store.BridgeUser) (*store.Store, error) { - var sentryReporter *sentry.Reporter // Sentry reporter is not used under unit tests. - - dbFile, err := os.CreateTemp(t.TempDir(), "bridge-store-db-*.db") - r.NoError(t, err, "could not get temporary file for store db") - r.NoError(t, dbFile.Close()) - - return store.New( - sentryReporter, - m.PanicHandler, - user, - m.eventListener, - cache.NewInMemoryCache(1<<20), - message.NewBuilder(runtime.NumCPU(), runtime.NumCPU()), - dbFile.Name(), - m.storeCache, - ) - }).AnyTimes() - m.storeMaker.EXPECT().Remove(gomock.Any()).AnyTimes() - - return m -} - -type fullStackReporter struct { - T testing.TB -} - -func (fr *fullStackReporter) Errorf(format string, args ...interface{}) { - fmt.Printf("err: "+format+"\n", args...) - fr.T.Fail() -} - -func (fr *fullStackReporter) Fatalf(format string, args ...interface{}) { - debug.PrintStack() - fmt.Printf("fail: "+format+"\n", args...) - fr.T.FailNow() -} - -func testNewUsersWithUsers(t *testing.T, m mocks) *Users { - m.credentialsStore.EXPECT().List().Return([]string{testCredentials.UserID, testCredentialsSplit.UserID}, nil) - mockLoadingConnectedUser(t, m, testCredentials) - mockLoadingConnectedUser(t, m, testCredentialsSplit) - mockEventLoopNoAction(m) - - return testNewUsers(t, m) -} - -func testNewUsers(t *testing.T, m mocks) *Users { //nolint:unparam - m.eventListener.EXPECT().ProvideChannel(events.UpgradeApplicationEvent) - m.eventListener.EXPECT().ProvideChannel(events.InternetConnChangedEvent) - - users := New(m.locator, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore, m.storeMaker) - - waitForEvents() - - return users -} - -func waitForEvents() { - // Wait for goroutine to add listener. - // E.g. calling login to invoke firstsync event. Functions can end sooner than - // goroutines call the listener mock. We need to wait a little bit before the end of - // the test to capture all event calls. This allows us to detect whether there were - // missing calls, or perhaps whether something was called too many times. - time.Sleep(100 * time.Millisecond) -} - -func cleanUpUsersData(b *Users) { - for _, user := range b.users { - _ = user.clearStore() - } -} - -func mockAddingConnectedUser(t *testing.T, m mocks) { - gomock.InOrder( - // Mock of users.FinishLogin. - m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil), - m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil), - m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUser, nil), - m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), - m.credentialsStore.EXPECT().Add("user", "username", testAuthRefresh.UID, testAuthRefresh.RefreshToken, testCredentials.MailboxPassword, []string{testPMAPIAddress.Email}).Return(testCredentials, nil), - m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), - ) - - mockInitConnectedUser(t, m) -} - -func mockLoadingConnectedUser(t *testing.T, m mocks, creds *credentials.Credentials) { - authRefresh := &pmapi.AuthRefresh{ - UID: "uid", - AccessToken: "acc", - RefreshToken: "ref", - } - - gomock.InOrder( - // Mock of users.loadUsersFromCredentialsStore. - m.credentialsStore.EXPECT().Get(creds.UserID).Return(creds, nil), - m.clientManager.EXPECT().NewClientWithRefresh(gomock.Any(), "uid", "acc").Return(m.pmapiClient, authRefresh, nil), - m.credentialsStore.EXPECT().UpdateToken(creds.UserID, authRefresh.UID, authRefresh.RefreshToken).Return(creds, nil), - ) - - mockInitConnectedUser(t, m) -} - -func mockInitConnectedUser(t *testing.T, m mocks) { - // Mock of user initialisation. - m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()) - m.pmapiClient.EXPECT().IsUnlocked().Return(true).AnyTimes() - m.pmapiClient.EXPECT().GetUser(gomock.Any()).Return(testPMAPIUser, nil) // load connected user - - // Mock of store initialisation. - gomock.InOrder( - m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return([]*pmapi.Label{}, nil), - m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil), - m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), - m.pmapiClient.EXPECT().GetUserKeyRing().Return(tests.MakeKeyRing(t), nil).AnyTimes(), - ) -} - -func mockLoadingDisconnectedUser(m mocks, creds *credentials.Credentials) { - gomock.InOrder( - // Mock of users.loadUsersFromCredentialsStore. - m.credentialsStore.EXPECT().Get(creds.UserID).Return(creds, nil), - m.clientManager.EXPECT().NewClient("", "", "", time.Time{}).Return(m.pmapiClient), - ) - - mockInitDisconnectedUser(m) -} - -func mockInitDisconnectedUser(m mocks) { - gomock.InOrder( - // Mock of user initialisation. - m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()), - - // Mock of store initialisation for the unauthorized user. - m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return(nil, pmapi.ErrUnauthorized), - m.pmapiClient.EXPECT().Addresses().Return(nil), - ) -} - -func mockEventLoopNoAction(m mocks) { - // Set up mocks for starting the store's event loop (in store.New). - // The event loop runs in another goroutine so this might happen at any time. - m.pmapiClient.EXPECT().GetEvent(gomock.Any(), "").Return(testPMAPIEvent, nil).AnyTimes() - m.pmapiClient.EXPECT().GetEvent(gomock.Any(), testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes() - m.pmapiClient.EXPECT().ListMessages(gomock.Any(), gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes() -} diff --git a/internal/vault/certs.go b/internal/vault/certs.go new file mode 100644 index 00000000..7c7b85c1 --- /dev/null +++ b/internal/vault/certs.go @@ -0,0 +1,19 @@ +package vault + +func (vault *Vault) GetBridgeTLSCert() []byte { + return vault.get().Certs.Bridge.Cert +} + +func (vault *Vault) GetBridgeTLSKey() []byte { + return vault.get().Certs.Bridge.Key +} + +func (vault *Vault) GetCertsInstalled() bool { + return vault.get().Certs.Installed +} + +func (vault *Vault) SetCertsInstalled(installed bool) error { + return vault.mod(func(data *Data) { + data.Certs.Installed = installed + }) +} diff --git a/internal/vault/certs_test.go b/internal/vault/certs_test.go new file mode 100644 index 00000000..58794973 --- /dev/null +++ b/internal/vault/certs_test.go @@ -0,0 +1,25 @@ +package vault_test + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVault_TLSCerts(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default bridge TLS certs. + require.NotEmpty(t, s.GetBridgeTLSCert()) + require.NotEmpty(t, s.GetBridgeTLSKey()) + + // Check the certificates are not installed. + require.False(t, s.GetCertsInstalled()) + + // Install the certificates. + require.NoError(t, s.SetCertsInstalled(true)) + + // Check the certificates are installed. + require.True(t, s.GetCertsInstalled()) +} diff --git a/internal/vault/cookies.go b/internal/vault/cookies.go new file mode 100644 index 00000000..bf72238b --- /dev/null +++ b/internal/vault/cookies.go @@ -0,0 +1,11 @@ +package vault + +func (vault *Vault) GetCookies() ([]byte, error) { + return vault.get().Cookies, nil +} + +func (vault *Vault) SetCookies(cookies []byte) error { + return vault.mod(func(data *Data) { + data.Cookies = cookies + }) +} diff --git a/internal/vault/cookies_test.go b/internal/vault/cookies_test.go new file mode 100644 index 00000000..bfc461ee --- /dev/null +++ b/internal/vault/cookies_test.go @@ -0,0 +1,25 @@ +package vault_test + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVault_Cookies(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default cookies are empty. + cookies, err := s.GetCookies() + require.NoError(t, err) + require.Empty(t, cookies) + + // Set some cookies. + require.NoError(t, s.SetCookies([]byte("something"))) + + // Check the cookies are as set. + newCookies, err := s.GetCookies() + require.NoError(t, err) + require.Equal(t, []byte("something"), newCookies) +} diff --git a/internal/vault/helper.go b/internal/vault/helper.go new file mode 100644 index 00000000..6a9337ca --- /dev/null +++ b/internal/vault/helper.go @@ -0,0 +1,41 @@ +package vault + +import ( + "encoding/json" + "errors" + "io/fs" + "os" + "path/filepath" +) + +type Keychain struct { + Helper string +} + +func GetHelper(vaultDir string) (string, error) { + var keychain Keychain + + if _, err := os.Stat(filepath.Join(vaultDir, "keychain.json")); errors.Is(err, fs.ErrNotExist) { + return "", nil + } + + b, err := os.ReadFile(filepath.Join(vaultDir, "keychain.json")) + if err != nil { + return "", err + } + + if err := json.Unmarshal(b, &keychain); err != nil { + return "", err + } + + return keychain.Helper, nil +} + +func SetHelper(vaultDir, helper string) error { + b, err := json.MarshalIndent(Keychain{Helper: helper}, "", " ") + if err != nil { + return err + } + + return os.WriteFile(filepath.Join(vaultDir, "keychain.json"), b, 0o600) +} diff --git a/internal/vault/settings.go b/internal/vault/settings.go new file mode 100644 index 00000000..7fb84b87 --- /dev/null +++ b/internal/vault/settings.go @@ -0,0 +1,186 @@ +package vault + +import ( + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/proton-bridge/v2/internal/updater" +) + +// GetIMAPPort sets the port that the IMAP server should listen on. +func (vault *Vault) GetIMAPPort() int { + return vault.get().Settings.IMAPPort +} + +// SetIMAPPort sets the port that the IMAP server should listen on. +func (vault *Vault) SetIMAPPort(port int) error { + return vault.mod(func(data *Data) { + data.Settings.IMAPPort = port + }) +} + +// GetSMTPPort sets the port that the SMTP server should listen on. +func (vault *Vault) GetSMTPPort() int { + return vault.get().Settings.SMTPPort +} + +// SetSMTPPort sets the port that the SMTP server should listen on. +func (vault *Vault) SetSMTPPort(port int) error { + return vault.mod(func(data *Data) { + data.Settings.SMTPPort = port + }) +} + +// GetIMAPSSL sets whether the IMAP server should use SSL. +func (vault *Vault) GetIMAPSSL() bool { + return vault.get().Settings.IMAPSSL +} + +// SetIMAPSSL sets whether the IMAP server should use SSL. +func (vault *Vault) SetIMAPSSL(ssl bool) error { + return vault.mod(func(data *Data) { + data.Settings.IMAPSSL = ssl + }) +} + +// GetSMTPSSL sets whether the SMTP server should use SSL. +func (vault *Vault) GetSMTPSSL() bool { + return vault.get().Settings.SMTPSSL +} + +// SetSMTPSSL sets whether the SMTP server should use SSL. +func (vault *Vault) SetSMTPSSL(ssl bool) error { + return vault.mod(func(data *Data) { + data.Settings.SMTPSSL = ssl + }) +} + +// GetGluonDir sets the directory where the gluon should store its data. +func (vault *Vault) GetGluonDir() string { + return vault.get().Settings.GluonDir +} + +// SetGluonDir sets the directory where the gluon should store its data. +func (vault *Vault) SetGluonDir(dir string) error { + return vault.mod(func(data *Data) { + data.Settings.GluonDir = dir + }) +} + +// GetUpdateChannel sets the update channel. +func (vault *Vault) GetUpdateChannel() updater.Channel { + return vault.get().Settings.UpdateChannel +} + +// SetUpdateChannel sets the update channel. +func (vault *Vault) SetUpdateChannel(channel updater.Channel) error { + return vault.mod(func(data *Data) { + data.Settings.UpdateChannel = channel + }) +} + +// GetUpdateRollout sets the update rollout. +func (vault *Vault) GetUpdateRollout() float64 { + return vault.get().Settings.UpdateRollout +} + +// SetUpdateRollout sets the update rollout. +func (vault *Vault) SetUpdateRollout(rollout float64) error { + return vault.mod(func(data *Data) { + data.Settings.UpdateRollout = rollout + }) +} + +// GetColorScheme sets the color scheme to be used by the bridge GUI. +func (vault *Vault) GetColorScheme() string { + return vault.get().Settings.ColorScheme +} + +// SetColorScheme sets the color scheme to be used by the bridge GUI. +func (vault *Vault) SetColorScheme(colorScheme string) error { + return vault.mod(func(data *Data) { + data.Settings.ColorScheme = colorScheme + }) +} + +// GetProxyAllowed sets whether the bridge is allowed to use alternative routing. +func (vault *Vault) GetProxyAllowed() bool { + return vault.get().Settings.ProxyAllowed +} + +// SetProxyAllowed sets whether the bridge is allowed to use alternative routing. +func (vault *Vault) SetProxyAllowed(allowed bool) error { + return vault.mod(func(data *Data) { + data.Settings.ProxyAllowed = allowed + }) +} + +// GetShowAllMail sets whether the bridge should show the All Mail folder. +func (vault *Vault) GetShowAllMail() bool { + return vault.get().Settings.ShowAllMail +} + +// SetShowAllMail sets whether the bridge should show the All Mail folder. +func (vault *Vault) SetShowAllMail(showAllMail bool) error { + return vault.mod(func(data *Data) { + data.Settings.ShowAllMail = showAllMail + }) +} + +// GetAutostart sets whether the bridge should autostart. +func (vault *Vault) GetAutostart() bool { + return vault.get().Settings.Autostart +} + +// SetAutostart sets whether the bridge should autostart. +func (vault *Vault) SetAutostart(autostart bool) error { + return vault.mod(func(data *Data) { + data.Settings.Autostart = autostart + }) +} + +// GetAutoUpdate sets whether the bridge should automatically update. +func (vault *Vault) GetAutoUpdate() bool { + return vault.get().Settings.AutoUpdate +} + +// SetAutoUpdate sets whether the bridge should automatically update. +func (vault *Vault) SetAutoUpdate(autoUpdate bool) error { + return vault.mod(func(data *Data) { + data.Settings.AutoUpdate = autoUpdate + }) +} + +// GetLastVersion returns the last version of the bridge that was run. +func (vault *Vault) GetLastVersion() *semver.Version { + return vault.get().Settings.LastVersion +} + +// SetLastVersion sets the last version of the bridge that was run. +func (vault *Vault) SetLastVersion(version *semver.Version) error { + return vault.mod(func(data *Data) { + data.Settings.LastVersion = version + }) +} + +// GetFirstStart sets whether this is the first time the bridge has been started. +func (vault *Vault) GetFirstStart() bool { + return vault.get().Settings.FirstStart +} + +// SetFirstStart sets whether this is the first time the bridge has been started. +func (vault *Vault) SetFirstStart(firstStart bool) error { + return vault.mod(func(data *Data) { + data.Settings.FirstStart = firstStart + }) +} + +// GetFirstStartGUI sets whether this is the first time the bridge GUI has been started. +func (vault *Vault) GetFirstStartGUI() bool { + return vault.get().Settings.FirstStartGUI +} + +// SetFirstStartGUI sets whether this is the first time the bridge GUI has been started. +func (vault *Vault) SetFirstStartGUI(firstStartGUI bool) error { + return vault.mod(func(data *Data) { + data.Settings.FirstStartGUI = firstStartGUI + }) +} diff --git a/internal/vault/settings_test.go b/internal/vault/settings_test.go new file mode 100644 index 00000000..65297dea --- /dev/null +++ b/internal/vault/settings_test.go @@ -0,0 +1,201 @@ +package vault_test + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/proton-bridge/v2/internal/updater" + "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/stretchr/testify/require" +) + +func TestVault_Settings_IMAP(t *testing.T) { + // Create a new test vault. + s := newVault(t) + + // Check the default IMAP port and SSL setting. + require.Equal(t, 1143, s.GetIMAPPort()) + require.Equal(t, false, s.GetIMAPSSL()) + + // Modify the IMAP port and SSL setting. + require.NoError(t, s.SetIMAPPort(1234)) + require.NoError(t, s.SetIMAPSSL(true)) + + // Check the new IMAP port and SSL setting. + require.Equal(t, 1234, s.GetIMAPPort()) + require.Equal(t, true, s.GetIMAPSSL()) +} + +func TestVault_Settings_SMTP(t *testing.T) { + // Create a new test vault. + s := newVault(t) + + // Check the default SMTP port and SSL setting. + require.Equal(t, 1025, s.GetSMTPPort()) + require.Equal(t, false, s.GetSMTPSSL()) + + // Modify the SMTP port and SSL setting. + require.NoError(t, s.SetSMTPPort(1234)) + require.NoError(t, s.SetSMTPSSL(true)) + + // Check the new SMTP port and SSL setting. + require.Equal(t, 1234, s.GetSMTPPort()) + require.Equal(t, true, s.GetSMTPSSL()) +} + +func TestVault_Settings_GluonDir(t *testing.T) { + // create a new test vault. + s, corrupt, err := vault.New(t.TempDir(), "/path/to/gluon", []byte("my secret key")) + require.NoError(t, err) + require.False(t, corrupt) + + // Check the default gluon dir. + require.Equal(t, "/path/to/gluon", s.GetGluonDir()) + + // Modify the gluon dir. + require.NoError(t, s.SetGluonDir("/tmp/gluon")) + + // Check the new gluon dir. + require.Equal(t, "/tmp/gluon", s.GetGluonDir()) +} + +func TestVault_Settings_UpdateChannel(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default update channel. + require.Equal(t, updater.StableChannel, s.GetUpdateChannel()) + + // Modify the update channel. + require.NoError(t, s.SetUpdateChannel(updater.EarlyChannel)) + + // Check the new update channel. + require.Equal(t, updater.EarlyChannel, s.GetUpdateChannel()) +} + +func TestVault_Settings_UpdateRollout(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default update rollout. + require.GreaterOrEqual(t, s.GetUpdateRollout(), float64(0)) + require.LessOrEqual(t, s.GetUpdateRollout(), float64(1)) + + // Modify the update rollout. + require.NoError(t, s.SetUpdateRollout(0.5)) + + // Check the new update rollout. + require.Equal(t, float64(0.5), s.GetUpdateRollout()) +} + +func TestVault_Settings_ColorScheme(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default color scheme. + require.Equal(t, "", s.GetColorScheme()) + + // Modify the color scheme. + require.NoError(t, s.SetColorScheme("dark")) + + // Check the new color scheme. + require.Equal(t, "dark", s.GetColorScheme()) +} + +func TestVault_Settings_ProxyAllowed(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default proxy allowed setting. + require.Equal(t, true, s.GetProxyAllowed()) + + // Modify the proxy allowed setting. + require.NoError(t, s.SetProxyAllowed(false)) + + // Check the new proxy allowed setting. + require.Equal(t, false, s.GetProxyAllowed()) +} + +func TestVault_Settings_ShowAllMail(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default show all mail setting. + require.Equal(t, true, s.GetShowAllMail()) + + // Modify the show all mail setting. + require.NoError(t, s.SetShowAllMail(false)) + + // Check the new show all mail setting. + require.Equal(t, false, s.GetShowAllMail()) +} + +func TestVault_Settings_Autostart(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default autostart setting. + require.Equal(t, false, s.GetAutostart()) + + // Modify the autostart setting. + require.NoError(t, s.SetAutostart(true)) + + // Check the new autostart setting. + require.Equal(t, true, s.GetAutostart()) +} + +func TestVault_Settings_AutoUpdate(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default auto update setting. + require.Equal(t, true, s.GetAutoUpdate()) + + // Modify the auto update setting. + require.NoError(t, s.SetAutoUpdate(false)) + + // Check the new auto update setting. + require.Equal(t, false, s.GetAutoUpdate()) +} + +func TestVault_Settings_LastVersion(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default first start value. + require.True(t, semver.MustParse("0.0.0").Equal(s.GetLastVersion())) + + // Modify the first start value. + require.NoError(t, s.SetLastVersion(semver.MustParse("1.2.3"))) + + // Check the new first start value. + require.True(t, semver.MustParse("1.2.3").Equal(s.GetLastVersion())) +} + +func TestVault_Settings_FirstStart(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default first start value. + require.Equal(t, true, s.GetFirstStart()) + + // Modify the first start value. + require.NoError(t, s.SetFirstStart(false)) + + // Check the new first start value. + require.Equal(t, false, s.GetFirstStart()) +} + +func TestVault_Settings_FirstStartGUI(t *testing.T) { + // create a new test vault. + s := newVault(t) + + // Check the default first start value. + require.Equal(t, true, s.GetFirstStartGUI()) + + // Modify the first start value. + require.NoError(t, s.SetFirstStartGUI(false)) + + // Check the new first start value. + require.Equal(t, false, s.GetFirstStartGUI()) +} diff --git a/internal/vault/store.go b/internal/vault/store.go new file mode 100644 index 00000000..e62897ee --- /dev/null +++ b/internal/vault/store.go @@ -0,0 +1,264 @@ +package vault + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "io/fs" + "math/rand" + "os" + "path/filepath" + + "github.com/ProtonMail/proton-bridge/v2/internal/certs" + "github.com/bradenaw/juniper/xslices" +) + +var ( + ErrInsecure = errors.New("the vault is insecure") + ErrCorrupt = errors.New("the vault is corrupt") +) + +type Vault struct { + path string + enc []byte + gcm cipher.AEAD +} + +// New constructs a new encrypted data vault at the given filepath using the given encryption key. +func New(vaultDir, gluonDir string, key []byte) (*Vault, bool, error) { + if err := os.MkdirAll(vaultDir, 0o700); err != nil { + return nil, false, err + } + + hash256 := sha256.Sum256(key) + + aes, err := aes.NewCipher(hash256[:]) + if err != nil { + return nil, false, err + } + + gcm, err := cipher.NewGCM(aes) + if err != nil { + return nil, false, err + } + + vault, corrupt, err := newVault(filepath.Join(vaultDir, "vault.enc"), gluonDir, gcm) + if err != nil { + return nil, false, err + } + + return vault, corrupt, nil +} + +// GetUserIDs returns the user IDs and usernames of all users in the vault. +func (vault *Vault) GetUserIDs() []string { + return xslices.Map(vault.get().Users, func(user UserData) string { + return user.UserID + }) +} + +// GetUserIDs returns the user IDs and usernames of all users in the vault. +func (vault *Vault) GetUser(userID string) (*User, error) { + if idx := xslices.IndexFunc(vault.get().Users, func(user UserData) bool { + return user.UserID == userID + }); idx < 0 { + return nil, errors.New("no such user") + } + + return &User{ + vault: vault, + userID: userID, + }, nil +} + +// AddUser creates a new user in the vault with the given ID and username. +// A bridge password is generated using the package's token generator. +func (vault *Vault) AddUser(userID, username, authUID, authRef string, keyPass []byte) (*User, error) { + if idx := xslices.IndexFunc(vault.get().Users, func(user UserData) bool { + return user.UserID == userID + }); idx >= 0 { + return nil, errors.New("user already exists") + } + + tok, err := RandomToken(16) + if err != nil { + return nil, err + } + + if err := vault.mod(func(data *Data) { + data.Users = append(data.Users, UserData{ + UserID: userID, + Username: username, + BridgePass: hex.EncodeToString(tok), + + AuthUID: authUID, + AuthRef: authRef, + KeyPass: keyPass, + }) + }); err != nil { + return nil, err + } + + return vault.GetUser(userID) +} + +// DeleteUser removes the given user from the vault. +func (vault *Vault) DeleteUser(userID string) error { + return vault.mod(func(data *Data) { + idx := xslices.IndexFunc(data.Users, func(user UserData) bool { + return user.UserID == userID + }) + + if idx < 0 { + return + } + + data.Users = append(data.Users[:idx], data.Users[idx+1:]...) + }) +} + +func newVault(path, gluonDir string, gcm cipher.AEAD) (*Vault, bool, error) { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + if _, err := initVault(path, gluonDir, gcm); err != nil { + return nil, false, err + } + } + + enc, err := os.ReadFile(path) + if err != nil { + return nil, false, err + } + + var corrupt bool + + if _, err := decrypt(gcm, enc); err != nil { + corrupt = true + + newEnc, err := initVault(path, gluonDir, gcm) + if err != nil { + return nil, false, err + } + + enc = newEnc + } + + return &Vault{path: path, enc: enc, gcm: gcm}, corrupt, nil +} + +func (vault *Vault) get() Data { + dec, err := decrypt(vault.gcm, vault.enc) + if err != nil { + panic(err) + } + + var data Data + + if err := json.Unmarshal(dec, &data); err != nil { + panic(err) + } + + return data +} + +func (vault *Vault) mod(fn func(data *Data)) error { + data := vault.get() + + fn(&data) + + return vault.set(data) +} + +func (vault *Vault) set(data Data) error { + dec, err := json.Marshal(data) + if err != nil { + return err + } + + enc, err := encrypt(vault.gcm, dec) + if err != nil { + return err + } + + vault.enc = enc + + return os.WriteFile(vault.path, vault.enc, 0o600) +} + +func (vault *Vault) getUser(userID string) UserData { + return vault.get().Users[xslices.IndexFunc(vault.get().Users, func(user UserData) bool { + return user.UserID == userID + })] +} + +func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error { + return vault.mod(func(data *Data) { + idx := xslices.IndexFunc(data.Users, func(user UserData) bool { + return user.UserID == userID + }) + + fn(&data.Users[idx]) + }) +} + +func initVault(path, gluonDir string, gcm cipher.AEAD) ([]byte, error) { + bridgeCert, err := newTLSCert() + if err != nil { + return nil, err + } + + dec, err := json.Marshal(Data{ + Settings: newDefaultSettings(gluonDir), + + Certs: Certs{ + Bridge: bridgeCert, + }, + }) + if err != nil { + return nil, err + } + + enc, err := encrypt(gcm, dec) + if err != nil { + return nil, err + } + + if err := os.WriteFile(path, enc, 0o600); err != nil { + return nil, err + } + + return enc, nil +} + +func decrypt(gcm cipher.AEAD, enc []byte) ([]byte, error) { + return gcm.Open(nil, enc[:gcm.NonceSize()], enc[gcm.NonceSize():], nil) +} + +func encrypt(gcm cipher.AEAD, data []byte) ([]byte, error) { + nonce := make([]byte, gcm.NonceSize()) + + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, data, nil), nil +} + +func newTLSCert() (Cert, error) { + template, err := certs.NewTLSTemplate() + if err != nil { + return Cert{}, err + } + + certPEM, keyPEM, err := certs.GenerateCert(template) + if err != nil { + return Cert{}, err + } + + return Cert{ + Cert: certPEM, + Key: keyPEM, + }, nil +} diff --git a/internal/vault/store_test.go b/internal/vault/store_test.go new file mode 100644 index 00000000..54749fff --- /dev/null +++ b/internal/vault/store_test.go @@ -0,0 +1,40 @@ +package vault_test + +import ( + "testing" + + "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/stretchr/testify/require" +) + +func TestVaultCorrupt(t *testing.T) { + vaultDir, gluonDir := t.TempDir(), t.TempDir() + + { + _, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key")) + require.NoError(t, err) + require.False(t, corrupt) + } + + { + _, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key")) + require.NoError(t, err) + require.False(t, corrupt) + } + + { + _, corrupt, err := vault.New(vaultDir, gluonDir, []byte("bad key")) + require.NoError(t, err) + require.True(t, corrupt) + } +} + +func newVault(t *testing.T) *vault.Vault { + t.Helper() + + s, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key")) + require.NoError(t, err) + require.False(t, corrupt) + + return s +} diff --git a/internal/vault/token.go b/internal/vault/token.go new file mode 100644 index 00000000..7bd70445 --- /dev/null +++ b/internal/vault/token.go @@ -0,0 +1,13 @@ +package vault + +import ( + "github.com/ProtonMail/gopenpgp/v2/crypto" +) + +// RandomToken is a function that returns a random token. +var RandomToken func(size int) ([]byte, error) + +// By default, we use crypto.RandomToken to generate tokens. +func init() { + RandomToken = crypto.RandomToken +} diff --git a/internal/vault/types.go b/internal/vault/types.go new file mode 100644 index 00000000..a1519dde --- /dev/null +++ b/internal/vault/types.go @@ -0,0 +1,88 @@ +package vault + +import ( + "math/rand" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/proton-bridge/v2/internal/updater" +) + +type Data struct { + Settings Settings + Users []UserData + Cookies []byte + Certs Certs +} + +type Certs struct { + Bridge Cert + Installed bool +} + +type Cert struct { + Cert, Key []byte +} + +type Settings struct { + GluonDir string + + IMAPPort int + SMTPPort int + IMAPSSL bool + SMTPSSL bool + + UpdateChannel updater.Channel + UpdateRollout float64 + + ColorScheme string + ProxyAllowed bool + ShowAllMail bool + Autostart bool + AutoUpdate bool + + LastVersion *semver.Version + FirstStart bool + FirstStartGUI bool +} + +// UserData holds information about a single bridge user. +// The user may or may not be logged in. +type UserData struct { + UserID string + Username string + + GluonID string + GluonKey []byte + BridgePass string + + AuthUID string + AuthRef string + KeyPass []byte + + EventID string + HasSync bool +} + +func newDefaultSettings(gluonDir string) Settings { + return Settings{ + GluonDir: gluonDir, + + IMAPPort: 1143, + SMTPPort: 1025, + IMAPSSL: false, + SMTPSSL: false, + + UpdateChannel: updater.DefaultUpdateChannel, + UpdateRollout: rand.Float64(), + + ColorScheme: "", + ProxyAllowed: true, + ShowAllMail: true, + Autostart: false, + AutoUpdate: true, + + LastVersion: semver.MustParse("0.0.0"), + FirstStart: true, + FirstStartGUI: true, + } +} diff --git a/internal/vault/user.go b/internal/vault/user.go new file mode 100644 index 00000000..116644f5 --- /dev/null +++ b/internal/vault/user.go @@ -0,0 +1,91 @@ +package vault + +type User struct { + vault *Vault + userID string +} + +func (user *User) UserID() string { + return user.vault.getUser(user.userID).UserID +} + +func (user *User) Username() string { + return user.vault.getUser(user.userID).Username +} + +func (user *User) GluonID() string { + return user.vault.getUser(user.userID).GluonID +} + +func (user *User) GluonKey() []byte { + return user.vault.getUser(user.userID).GluonKey +} + +func (user *User) BridgePass() string { + return user.vault.getUser(user.userID).BridgePass +} + +func (user *User) AuthUID() string { + return user.vault.getUser(user.userID).AuthUID +} + +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) UpdateKeyPass(keyPass []byte) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.KeyPass = keyPass + }) +} + +// UpdateAuth updates the auth secrets for the given user. +func (user *User) UpdateAuth(authUID, authRef string) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.AuthUID = authUID + data.AuthRef = authRef + }) +} + +// UpdateGluonData updates the gluon ID and key for the given user. +func (user *User) UpdateGluonData(gluonID string, gluonKey []byte) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.GluonID = gluonID + data.GluonKey = gluonKey + }) +} + +// UpdateEventID updates the event ID for the given user. +func (user *User) UpdateEventID(eventID string) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.EventID = eventID + }) +} + +// UpdateSync updates the sync state for the given user. +func (user *User) UpdateSync(hasSync bool) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.HasSync = hasSync + }) +} + +// Clear clears the secrets for the given user. +func (user *User) Clear() error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.AuthUID = "" + data.AuthRef = "" + data.KeyPass = nil + }) +} diff --git a/internal/vault/user_test.go b/internal/vault/user_test.go new file mode 100644 index 00000000..28863acf --- /dev/null +++ b/internal/vault/user_test.go @@ -0,0 +1,84 @@ +package vault_test + +import ( + "encoding/hex" + "testing" + + "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/stretchr/testify/require" +) + +func TestUser(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) + + // Set auth information for user 1 and 2. + user1, err := s.AddUser("userID1", "user1", "authUID1", "authRef1", []byte("keyPass1")) + require.NoError(t, err) + user2, err := s.AddUser("userID2", "user2", "authUID2", "authRef2", []byte("keyPass2")) + require.NoError(t, err) + + // Set event IDs for user 1 and 2. + require.NoError(t, user1.UpdateEventID("eventID1")) + require.NoError(t, user2.UpdateEventID("eventID2")) + + // Set sync state for user 1 and 2. + require.NoError(t, user1.UpdateSync(true)) + require.NoError(t, user2.UpdateSync(false)) + + // Set gluon data for user 1 and 2. + require.NoError(t, user1.UpdateGluonData("gluonID1", []byte("gluonKey1"))) + require.NoError(t, user2.UpdateGluonData("gluonID2", []byte("gluonKey2"))) + + // List available users. + require.ElementsMatch(t, []string{"userID1", "userID2"}, s.GetUserIDs()) + + // Get auth information for user 1. + require.Equal(t, "userID1", user1.UserID()) + require.Equal(t, "user1", user1.Username()) + require.Equal(t, "gluonID1", user1.GluonID()) + require.Equal(t, []byte("gluonKey1"), user1.GluonKey()) + require.Equal(t, hex.EncodeToString([]byte("token")), user1.BridgePass()) + require.Equal(t, "authUID1", user1.AuthUID()) + require.Equal(t, "authRef1", user1.AuthRef()) + require.Equal(t, []byte("keyPass1"), user1.KeyPass()) + require.Equal(t, "eventID1", user1.EventID()) + require.Equal(t, true, user1.HasSync()) + + // Get auth information for user 2. + require.Equal(t, "userID2", user2.UserID()) + require.Equal(t, "user2", user2.Username()) + require.Equal(t, "gluonID2", user2.GluonID()) + require.Equal(t, []byte("gluonKey2"), user2.GluonKey()) + require.Equal(t, hex.EncodeToString([]byte("token")), user2.BridgePass()) + require.Equal(t, "authUID2", user2.AuthUID()) + require.Equal(t, "authRef2", user2.AuthRef()) + require.Equal(t, []byte("keyPass2"), user2.KeyPass()) + require.Equal(t, "eventID2", user2.EventID()) + require.Equal(t, false, user2.HasSync()) + + // Clear the users. + require.NoError(t, user1.Clear()) + require.NoError(t, user2.Clear()) + + // Their secrets should now be cleared. + require.Equal(t, "", user1.AuthUID()) + require.Equal(t, "", user1.AuthRef()) + require.Empty(t, user1.KeyPass()) + + // Get auth information for user 2. + require.Equal(t, "", user2.AuthUID()) + require.Equal(t, "", user2.AuthRef()) + require.Empty(t, user2.KeyPass()) + + // Delete auth information for user 1. + require.NoError(t, s.DeleteUser("userID1")) + + // List available userIDs. User 1 should be gone. + require.ElementsMatch(t, []string{"userID2"}, s.GetUserIDs()) +} diff --git a/internal/versioner/version_test.go b/internal/versioner/version_test.go index 881dab0f..c15ccefd 100644 --- a/internal/versioner/version_test.go +++ b/internal/versioner/version_test.go @@ -26,21 +26,19 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/v2/pkg/sum" - tests "github.com/ProtonMail/proton-bridge/v2/test" + "github.com/ProtonMail/proton-bridge/v2/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestVerifyFiles(t *testing.T) { - tempDir, err := os.MkdirTemp("", "verify-test") - require.NoError(t, err) - + dir := t.TempDir() version := &Version{ version: semver.MustParse("1.2.3"), - path: tempDir, + path: dir, } - kr := createSignedFiles(t, tempDir, + kr := createSignedFiles(t, dir, "f1.txt", "f2.png", "f3.dat", @@ -52,15 +50,14 @@ func TestVerifyFiles(t *testing.T) { } func TestVerifyWithBadFile(t *testing.T) { - tempDir, err := os.MkdirTemp("", "verify-test") - require.NoError(t, err) + dir := t.TempDir() version := &Version{ version: semver.MustParse("1.2.3"), - path: tempDir, + path: dir, } - kr := createSignedFiles(t, tempDir, + kr := createSignedFiles(t, dir, "f1.txt", "f2.png", "f3.bad", @@ -68,22 +65,20 @@ func TestVerifyWithBadFile(t *testing.T) { filepath.Join("sub", "f5.tgz"), ) - badKeyRing := tests.MakeKeyRing(t) - signFile(t, filepath.Join(tempDir, "f3.bad"), badKeyRing) + signFile(t, filepath.Join(dir, "f3.bad"), utils.MakeKeyRing(t)) assert.Error(t, version.VerifyFiles(kr)) } func TestVerifyWithBadSubFile(t *testing.T) { - tempDir, err := os.MkdirTemp("", "verify-test") - require.NoError(t, err) + dir := t.TempDir() version := &Version{ version: semver.MustParse("1.2.3"), - path: tempDir, + path: dir, } - kr := createSignedFiles(t, tempDir, + kr := createSignedFiles(t, dir, "f1.txt", "f2.png", "f3.dat", @@ -91,14 +86,13 @@ func TestVerifyWithBadSubFile(t *testing.T) { filepath.Join("sub", "f5.bad"), ) - badKeyRing := tests.MakeKeyRing(t) - signFile(t, filepath.Join(tempDir, "sub", "f5.bad"), badKeyRing) + signFile(t, filepath.Join(dir, "sub", "f5.bad"), utils.MakeKeyRing(t)) assert.Error(t, version.VerifyFiles(kr)) } func createSignedFiles(t *testing.T, root string, paths ...string) *crypto.KeyRing { - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) for _, path := range paths { makeFile(t, filepath.Join(root, path)) diff --git a/internal/versioner/versioner_test.go b/internal/versioner/versioner_test.go index dafe1acf..fef55469 100644 --- a/internal/versioner/versioner_test.go +++ b/internal/versioner/versioner_test.go @@ -28,25 +28,24 @@ import ( ) func TestListVersions(t *testing.T) { - updates, err := os.MkdirTemp("", "updates") - require.NoError(t, err) + dir := t.TempDir() - v := newTestVersioner(t, "myCoolApp", updates, "2.3.4-beta", "2.3.4", "2.3.5", "2.4.0") + v := newTestVersioner(t, "myCoolApp", dir, "2.3.4-beta", "2.3.4", "2.3.5", "2.4.0") versions, err := v.ListVersions() require.NoError(t, err) assert.Equal(t, semver.MustParse("2.4.0"), versions[0].version) - assert.Equal(t, filepath.Join(updates, "2.4.0"), versions[0].path) + assert.Equal(t, filepath.Join(dir, "2.4.0"), versions[0].path) assert.Equal(t, semver.MustParse("2.3.5"), versions[1].version) - assert.Equal(t, filepath.Join(updates, "2.3.5"), versions[1].path) + assert.Equal(t, filepath.Join(dir, "2.3.5"), versions[1].path) assert.Equal(t, semver.MustParse("2.3.4"), versions[2].version) - assert.Equal(t, filepath.Join(updates, "2.3.4"), versions[2].path) + assert.Equal(t, filepath.Join(dir, "2.3.4"), versions[2].path) assert.Equal(t, semver.MustParse("2.3.4-beta"), versions[3].version) - assert.Equal(t, filepath.Join(updates, "2.3.4-beta"), versions[3].path) + assert.Equal(t, filepath.Join(dir, "2.3.4-beta"), versions[3].path) } func newTestVersioner(t *testing.T, exeName, updates string, versions ...string) *Versioner { diff --git a/pkg/files/removal_test.go b/pkg/files/removal_test.go index 4f3cdcd5..0f31b040 100644 --- a/pkg/files/removal_test.go +++ b/pkg/files/removal_test.go @@ -92,8 +92,7 @@ func TestRemoveWithExceptions(t *testing.T) { } func newTestDir(t *testing.T, subdirs ...string) string { - dir, err := os.MkdirTemp("", "test-files-dir") - require.NoError(t, err) + dir := t.TempDir() for _, target := range subdirs { require.NoError(t, os.MkdirAll(filepath.Join(dir, target), 0o700)) diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index 756ef2cd..64859f32 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -23,7 +23,6 @@ import ( "fmt" "sync" - "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" "github.com/docker/docker-credential-helpers/credentials" ) @@ -48,20 +47,19 @@ var ( ) // NewKeychain creates a new native keychain. -func NewKeychain(s *settings.Settings, keychainName string) (*Keychain, error) { +func NewKeychain(preferred, keychainName string) (*Keychain, error) { // There must be at least one keychain helper available. if len(Helpers) < 1 { return nil, ErrNoKeychain } // If the preferred keychain is unsupported, fallback to the default one. - // NOTE: Maybe we want to error out here and show something in the GUI instead? - if _, ok := Helpers[s.Get(settings.PreferredKeychainKey)]; !ok { - s.Set(settings.PreferredKeychainKey, defaultHelper) + if _, ok := Helpers[preferred]; !ok { + preferred = defaultHelper } // Load the user's preferred keychain helper. - helperConstructor, ok := Helpers[s.Get(settings.PreferredKeychainKey)] + helperConstructor, ok := Helpers[preferred] if !ok { return nil, ErrNoKeychain } diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go deleted file mode 100644 index dcb932a3..00000000 --- a/pkg/listener/listener.go +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package listener - -import ( - "sync" - "time" - - "github.com/sirupsen/logrus" -) - -var log = logrus.WithField("pkg", "bridgeUtils/listener") //nolint:gochecknoglobals - -// Listener has a list of channels watching for updates. -type Listener interface { - SetLimit(eventName string, limit time.Duration) - ProvideChannel(eventName string) <-chan string - Add(eventName string, channel chan<- string) - Remove(eventName string, channel chan<- string) - Emit(eventName string, data string) - SetBuffer(eventName string) - RetryEmit(eventName string) - Book(eventName string) -} - -type listener struct { - channels map[string][]chan<- string - limits map[string]time.Duration - lastEmits map[string]map[string]time.Time - buffered map[string][]string - lock *sync.RWMutex -} - -// New returns a new Listener which initially has no topics. -func New() Listener { - return &listener{ - channels: nil, - limits: make(map[string]time.Duration), - lastEmits: make(map[string]map[string]time.Time), - buffered: make(map[string][]string), - lock: &sync.RWMutex{}, - } -} - -// Book wil create the list of channels for specific eventName. This should be -// used when there is not always listening channel available and it should not -// be logged when no channel is awaiting an emitted event. -func (l *listener) Book(eventName string) { - if l.channels == nil { - l.channels = make(map[string][]chan<- string) - } - if _, ok := l.channels[eventName]; !ok { - l.channels[eventName] = []chan<- string{} - } - log.WithField("name", eventName).Debug("Channel booked") -} - -// SetLimit sets the limit for the `eventName`. When the same event (name and data) -// is emitted within last time duration (`limit`), event is dropped. Zero limit clears -// the limit for the specific `eventName`. -func (l *listener) SetLimit(eventName string, limit time.Duration) { - l.lock.Lock() - defer l.lock.Unlock() - - if limit == 0 { - delete(l.limits, eventName) - return - } - l.limits[eventName] = limit -} - -// ProvideChannel creates new channel, adds it to listener and sends to it -// bufferent events. -func (l *listener) ProvideChannel(eventName string) <-chan string { - ch := make(chan string) - l.Add(eventName, ch) - l.RetryEmit(eventName) - return ch -} - -// Add adds an event listener. -func (l *listener) Add(eventName string, channel chan<- string) { - l.lock.Lock() - defer l.lock.Unlock() - - if l.channels == nil { - l.channels = make(map[string][]chan<- string) - } - - log := log.WithField("name", eventName).WithField("i", len(l.channels[eventName])) - l.channels[eventName] = append(l.channels[eventName], channel) - log.Debug("Added event listener") -} - -// Remove removes an event listener. -func (l *listener) Remove(eventName string, channel chan<- string) { - l.lock.Lock() - defer l.lock.Unlock() - - if _, ok := l.channels[eventName]; ok { - for i := range l.channels[eventName] { - if l.channels[eventName][i] == channel { - l.channels[eventName] = append(l.channels[eventName][:i], l.channels[eventName][i+1:]...) - break - } - } - } -} - -// Emit emits an event in parallel to all listeners (channels). -func (l *listener) Emit(eventName string, data string) { - l.lock.Lock() - defer l.lock.Unlock() - - l.emit(eventName, data, false) -} - -func (l *listener) emit(eventName, data string, isReEmit bool) { - if !l.shouldEmit(eventName, data) { - log.Warn("Emit of ", eventName, " with data ", data, " skipped") - return - } - - if _, ok := l.channels[eventName]; ok { - for i, handler := range l.channels[eventName] { - go func(handler chan<- string, i int) { - log := log.WithField("name", eventName).WithField("i", i).WithField("data", data) - log.Debug("Send event") - handler <- data - log.Debug("Event sent") - }(handler, i) - } - } else if !isReEmit { - if bufferedData, ok := l.buffered[eventName]; ok { - l.buffered[eventName] = append(bufferedData, data) - log.Debugf("Buffering event %s data %s", eventName, data) - } else { - log.Warnf("No channel is listening to %s data %s", eventName, data) - } - } -} - -func (l *listener) shouldEmit(eventName, data string) bool { - if _, ok := l.limits[eventName]; !ok { - return true - } - - l.clearLastEmits() - - if eventLastEmits, ok := l.lastEmits[eventName]; ok { - if _, ok := eventLastEmits[data]; ok { - return false - } - } else { - l.lastEmits[eventName] = make(map[string]time.Time) - } - - l.lastEmits[eventName][data] = time.Now() - return true -} - -func (l *listener) clearLastEmits() { - for eventName, lastEmits := range l.lastEmits { - limit, ok := l.limits[eventName] - if !ok { // Limits were disabled. - delete(l.lastEmits, eventName) - continue - } - for key, lastEmit := range lastEmits { - if time.Since(lastEmit) > limit { - delete(lastEmits, key) - } - } - } -} - -func (l *listener) SetBuffer(eventName string) { - l.lock.Lock() - defer l.lock.Unlock() - - if _, ok := l.buffered[eventName]; !ok { - l.buffered[eventName] = []string{} - } -} - -func (l *listener) RetryEmit(eventName string) { - l.lock.Lock() - defer l.lock.Unlock() - - if _, ok := l.channels[eventName]; !ok || len(l.channels[eventName]) == 0 { - return - } - if bufferedData, ok := l.buffered[eventName]; ok { - for _, data := range bufferedData { - l.emit(eventName, data, true) - } - l.buffered[eventName] = []string{} - } -} diff --git a/pkg/listener/listener_test.go b/pkg/listener/listener_test.go deleted file mode 100644 index 4f34b68f..00000000 --- a/pkg/listener/listener_test.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package listener - -import ( - "fmt" - "testing" - "time" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" -) - -const minEventReceiveTime = 100 * time.Millisecond - -func Example() { - eventListener := New() - - ch := make(chan string) - eventListener.Add("eventname", ch) - for eventdata := range ch { - fmt.Println(eventdata + " world") - } - - eventListener.Emit("eventname", "hello") -} - -func TestAddAndEmitSameEvent(t *testing.T) { - listener, channel := newListener() - - listener.Emit("event", "hello!") - checkChannelEmitted(t, channel, "hello!") -} - -func TestAddAndEmitDifferentEvent(t *testing.T) { - listener, channel := newListener() - - listener.Emit("other", "hello!") - checkChannelNotEmitted(t, channel) -} - -func TestAddAndRemove(t *testing.T) { - listener := New() - - channel := make(chan string) - listener.Add("event", channel) - listener.Emit("event", "hello!") - checkChannelEmitted(t, channel, "hello!") - - listener.Remove("event", channel) - listener.Emit("event", "hello!") - - checkChannelNotEmitted(t, channel) -} - -func TestNoLimit(t *testing.T) { - listener, channel := newListener() - - listener.Emit("event", "hello!") - checkChannelEmitted(t, channel, "hello!") - - listener.Emit("event", "hello!") - checkChannelEmitted(t, channel, "hello!") -} - -func TestLimit(t *testing.T) { - listener, channel := newListener() - listener.SetLimit("event", 1*time.Second) - - channel2 := make(chan string) - listener.Add("event", channel2) - - listener.Emit("event", "hello!") - checkChannelEmitted(t, channel, "hello!") - checkChannelEmitted(t, channel2, "hello!") - - listener.Emit("event", "hello!") - checkChannelNotEmitted(t, channel) - checkChannelNotEmitted(t, channel2) - - time.Sleep(1 * time.Second) - - listener.Emit("event", "hello!") - checkChannelEmitted(t, channel, "hello!") - checkChannelEmitted(t, channel2, "hello!") -} - -func TestLimitDifferentData(t *testing.T) { - listener, channel := newListener() - listener.SetLimit("event", 1*time.Second) - - listener.Emit("event", "hello!") - checkChannelEmitted(t, channel, "hello!") - - listener.Emit("event", "hello?") - checkChannelEmitted(t, channel, "hello?") -} - -func TestReEmit(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) - listener := New() - listener.Emit("event", "hello?") - - listener.SetBuffer("event") - listener.SetBuffer("other") - - listener.Emit("event", "hello1") - listener.Emit("event", "hello2") - listener.Emit("other", "hello!") - listener.Emit("event", "hello3") - listener.Emit("other", "hello!") - - eventCH := make(chan string, 3) - listener.Add("event", eventCH) - - otherCH := make(chan string) - listener.Add("other", otherCH) - - listener.RetryEmit("event") - listener.RetryEmit("other") - time.Sleep(time.Millisecond) - - receivedEvents := map[string]int{} - for i := 0; i < 5; i++ { - select { - case res := <-eventCH: - receivedEvents[res]++ - case res := <-otherCH: - receivedEvents[res+":other"]++ - case <-time.After(minEventReceiveTime): - t.Fatalf("Channel not emitted %d times", i+1) - } - } - expectedEvents := map[string]int{"hello1": 1, "hello2": 1, "hello3": 1, "hello!:other": 2} - require.Equal(t, expectedEvents, receivedEvents) -} - -func newListener() (Listener, chan string) { - listener := New() - - channel := make(chan string) - listener.Add("event", channel) - - return listener, channel -} - -func checkChannelEmitted(t testing.TB, channel chan string, expectedData string) { - select { - case res := <-channel: - require.Equal(t, expectedData, res) - case <-time.After(minEventReceiveTime): - t.Fatalf("Channel not emitted with expected data: %s", expectedData) - } -} - -func checkChannelNotEmitted(t testing.TB, channel chan string) { - select { - case res := <-channel: - t.Fatalf("Channel emitted with a unexpected response: %s", res) - case <-time.After(minEventReceiveTime): - } -} diff --git a/pkg/message/boundary_reader.go b/pkg/message/boundary_reader.go deleted file mode 100644 index 538ee4e0..00000000 --- a/pkg/message/boundary_reader.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "bufio" - "bytes" - "io" -) - -type boundaryReader struct { - reader *bufio.Reader - - closed, first bool - skipped int - - nl []byte // "\r\n" or "\n" (set after seeing first boundary line) - nlDashBoundary []byte // nl + "--boundary" - dashBoundaryDash []byte // "--boundary--" - dashBoundary []byte // "--boundary" -} - -func newBoundaryReader(r *bufio.Reader, boundary string) (br *boundaryReader, err error) { - b := []byte("\r\n--" + boundary + "--") - br = &boundaryReader{ - reader: r, - closed: false, - first: true, - nl: b[:2], - nlDashBoundary: b[:len(b)-2], - dashBoundaryDash: b[2:], - dashBoundary: b[2 : len(b)-2], - } - err = br.writeNextPartTo(nil) - return -} - -// writeNextPartTo will copy the the bytes of next part and write them to -// writer. Will return EOF if the underlying reader is empty. -func (br *boundaryReader) writeNextPartTo(part io.Writer) (err error) { - if br.closed { - return io.EOF - } - - var line, slice []byte - br.skipped = 0 - - for { - slice, err = br.reader.ReadSlice('\n') - line = append(line, slice...) - if err == bufio.ErrBufferFull { - continue - } - - br.skipped += len(line) - - if err == io.EOF && br.isFinalBoundary(line) { - err = nil - br.closed = true - return - } - - if err != nil { - return - } - - if br.isBoundaryDelimiterLine(line) { - br.first = false - return - } - - if br.isFinalBoundary(line) { - br.closed = true - return - } - - if part != nil { - if _, err = part.Write(line); err != nil { - return - } - } - - line = []byte{} - } -} - -func (br *boundaryReader) isFinalBoundary(line []byte) bool { - if !bytes.HasPrefix(line, br.dashBoundaryDash) { - return false - } - rest := line[len(br.dashBoundaryDash):] - rest = skipLWSPChar(rest) - return len(rest) == 0 || bytes.Equal(rest, br.nl) -} - -func (br *boundaryReader) isBoundaryDelimiterLine(line []byte) (ret bool) { - if !bytes.HasPrefix(line, br.dashBoundary) { - return false - } - rest := line[len(br.dashBoundary):] - rest = skipLWSPChar(rest) - - if br.first && len(rest) == 1 && rest[0] == '\n' { - br.nl = br.nl[1:] - br.nlDashBoundary = br.nlDashBoundary[1:] - } - return bytes.Equal(rest, br.nl) -} - -func skipLWSPChar(b []byte) []byte { - for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') { - b = b[1:] - } - return b -} diff --git a/pkg/message/build.go b/pkg/message/build.go index 75e6e8cd..789fd502 100644 --- a/pkg/message/build.go +++ b/pkg/message/build.go @@ -18,14 +18,22 @@ package message import ( - "context" - "io" - "sync" + "bytes" + "encoding/base64" + "mime" + "net/mail" + "strings" + "time" + "unicode/utf8" + "github.com/ProtonMail/gluon/rfc822" + "github.com/ProtonMail/go-rfc5322" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/ProtonMail/proton-bridge/v2/pkg/pool" + "github.com/ProtonMail/proton-bridge/v2/pkg/algo" + "github.com/emersion/go-message" + "github.com/emersion/go-message/textproto" "github.com/pkg/errors" + "gitlab.protontech.ch/go/liteapi" ) var ( @@ -33,199 +41,526 @@ var ( ErrNoSuchKeyRing = errors.New("the keyring to decrypt this message could not be found") ) -const ( - BackgroundPriority = 1 << iota - ForegroundPriority -) +// InternalIDDomain is used as a placeholder for reference/message ID headers to improve compatibility with various clients. +const InternalIDDomain = `protonmail.internalid` -type Builder struct { - pool *pool.Pool - jobs map[string]*Job - lock sync.Mutex -} +func BuildRFC822(kr *crypto.KeyRing, msg liteapi.Message, attData map[string][]byte, opts JobOptions) ([]byte, error) { + switch { + case len(msg.Attachments) > 0: + return buildMultipartRFC822(kr, msg, attData, opts) -type Fetcher interface { - GetMessage(context.Context, string) (*pmapi.Message, error) - GetAttachment(context.Context, string) (io.ReadCloser, error) - KeyRingForAddressID(string) (*crypto.KeyRing, error) -} + case msg.MIMEType == "multipart/mixed": + return buildPGPRFC822(kr, msg, opts) -// NewBuilder creates a new builder which manages the given number of fetch/attach/build workers. -// - fetchWorkers: the number of workers which fetch messages from API -// - attachWorkers: the number of workers which fetch attachments from API. -// -// The returned builder is ready to handle jobs -- see (*Builder).NewJob for more information. -// -// Call (*Builder).Done to shut down the builder and stop all workers. -func NewBuilder(fetchWorkers, attachmentWorkers int) *Builder { - attachmentPool := pool.New(attachmentWorkers, newAttacherWorkFunc()) - - fetcherPool := pool.New(fetchWorkers, newFetcherWorkFunc(attachmentPool)) - - return &Builder{ - pool: fetcherPool, - jobs: make(map[string]*Job), + default: + return buildSimpleRFC822(kr, msg, opts) } } -func (builder *Builder) NewJob(ctx context.Context, fetcher Fetcher, messageID string, prio int) (*Job, pool.DoneFunc) { - return builder.NewJobWithOptions(ctx, fetcher, messageID, JobOptions{}, prio) -} - -func (builder *Builder) NewJobWithOptions(ctx context.Context, fetcher Fetcher, messageID string, opts JobOptions, prio int) (*Job, pool.DoneFunc) { - builder.lock.Lock() - defer builder.lock.Unlock() - - if job, ok := builder.jobs[messageID]; ok { - if job.GetPriority() < prio { - job.SetPriority(prio) +func buildSimpleRFC822(kr *crypto.KeyRing, msg liteapi.Message, opts JobOptions) ([]byte, error) { + dec, err := msg.Decrypt(kr) + if err != nil { + if !opts.IgnoreDecryptionErrors { + return nil, errors.Wrap(ErrDecryptionFailed, err.Error()) } - return job, job.done + return buildMultipartRFC822(kr, msg, nil, opts) } - job, done := builder.pool.NewJob( - &fetchReq{ - ctx: ctx, - fetcher: fetcher, - messageID: messageID, - options: opts, - }, - prio, - ) + hdr := getTextPartHeader(getMessageHeader(msg, opts), dec, msg.MIMEType) - buildDone := func() { - builder.lock.Lock() - defer builder.lock.Unlock() + buf := new(bytes.Buffer) - // Remove the job from the builder. - delete(builder.jobs, messageID) - - // And mark it as done. - done() - } - - buildJob := &Job{ - Job: job, - done: buildDone, - } - - builder.jobs[messageID] = buildJob - - return buildJob, buildDone -} - -func (builder *Builder) Done() { - // NOTE(GODT-1158): Stop worker pool. -} - -type fetchReq struct { - ctx context.Context - fetcher Fetcher - messageID string - options JobOptions -} - -type attachReq struct { - ctx context.Context - fetcher Fetcher - message *pmapi.Message -} - -type Job struct { - *pool.Job - - done pool.DoneFunc -} - -func (job *Job) GetResult() ([]byte, error) { - res, err := job.Job.GetResult() + w, err := message.CreateWriter(buf, hdr) if err != nil { return nil, err } - return res.([]byte), nil //nolint:forcetypeassert -} - -// NOTE: This is not used because it is actually not doing what was expected: It -// downloads all the attachments which belongs to one message sequentially -// within one goroutine. We should have one job per one attachment. This doesn't look -// like a bottle neck right now. -func newAttacherWorkFunc() pool.WorkFunc { - return func(payload interface{}, prio int) (interface{}, error) { - req, ok := payload.(*attachReq) - if !ok { - panic("bad payload type") - } - - res := make(map[string][]byte) - - for _, att := range req.message.Attachments { - rc, err := req.fetcher.GetAttachment(req.ctx, att.ID) - if err != nil { - return nil, err - } - - b, err := io.ReadAll(rc) - if err != nil { - return nil, err - } - - if err := rc.Close(); err != nil { - return nil, err - } - - res[att.ID] = b - } - - return res, nil + if _, err := w.Write(dec); err != nil { + return nil, err } + + if err := w.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil } -func newFetcherWorkFunc(attachmentPool *pool.Pool) pool.WorkFunc { - return func(payload interface{}, prio int) (interface{}, error) { - req, ok := payload.(*fetchReq) - if !ok { - panic("bad payload type") - } +func buildMultipartRFC822( + kr *crypto.KeyRing, + msg liteapi.Message, + attData map[string][]byte, + opts JobOptions, +) ([]byte, error) { + boundary := newBoundary(msg.ID) - msg, err := req.fetcher.GetMessage(req.ctx, req.messageID) - if err != nil { + hdr := getMessageHeader(msg, opts) + + hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()}) + + buf := new(bytes.Buffer) + + w, err := message.CreateWriter(buf, hdr) + if err != nil { + return nil, err + } + + var ( + inlineAtts []liteapi.Attachment + inlineData [][]byte + attachAtts []liteapi.Attachment + attachData [][]byte + ) + + for _, att := range msg.Attachments { + if att.Disposition == liteapi.InlineDisposition { + inlineAtts = append(inlineAtts, att) + inlineData = append(inlineData, attData[att.ID]) + } else { + attachAtts = append(attachAtts, att) + attachData = append(attachData, attData[att.ID]) + } + } + + if len(inlineAtts) > 0 { + if err := writeRelatedParts(w, kr, boundary, msg, inlineAtts, inlineData, opts); err != nil { return nil, err } + } else if err := writeTextPart(w, kr, msg, opts); err != nil { + return nil, err + } - attData := make(map[string][]byte) + for i, att := range attachAtts { + if err := writeAttachmentPart(w, kr, att, attachData[i], opts); err != nil { + return nil, err + } + } - for _, att := range msg.Attachments { - // NOTE: Potential place for optimization: - // Use attachmentPool to download each attachment in - // separate parallel job. It is not straightforward - // because we need to make sure we call attachment-job-done - // function in case of any error or after we collect all - // attachment bytes asynchronously. - rc, err := req.fetcher.GetAttachment(req.ctx, att.ID) - if err != nil { - return nil, err - } + if err := w.Close(); err != nil { + return nil, err + } - b, err := io.ReadAll(rc) - if err != nil { - _ = rc.Close() - return nil, err - } + return buf.Bytes(), nil +} - if err := rc.Close(); err != nil { - return nil, err - } - - attData[att.ID] = b +func writeTextPart( + w *message.Writer, + kr *crypto.KeyRing, + msg liteapi.Message, + opts JobOptions, +) error { + dec, err := msg.Decrypt(kr) + if err != nil { + if !opts.IgnoreDecryptionErrors { + return errors.Wrap(ErrDecryptionFailed, err.Error()) } - kr, err := req.fetcher.KeyRingForAddressID(msg.AddressID) - if err != nil { - return nil, ErrNoSuchKeyRing + return writeCustomTextPart(w, msg, err) + } + + return writePart(w, getTextPartHeader(message.Header{}, dec, msg.MIMEType), dec) +} + +func writeAttachmentPart( + w *message.Writer, + kr *crypto.KeyRing, + att liteapi.Attachment, + attData []byte, + opts JobOptions, +) error { + kps, err := base64.StdEncoding.DecodeString(att.KeyPackets) + if err != nil { + return err + } + + msg := crypto.NewPGPSplitMessage(kps, attData).GetPGPMessage() + + dec, err := kr.Decrypt(msg, nil, crypto.GetUnixTime()) + if err != nil { + if !opts.IgnoreDecryptionErrors { + return errors.Wrap(ErrDecryptionFailed, err.Error()) } - return buildRFC822(kr, msg, attData, req.options) + log. + WithField("attID", att.ID). + WithError(err). + Warn("Attachment decryption failed") + + return writeCustomAttachmentPart(w, att, msg, err) + } + + return writePart(w, getAttachmentPartHeader(att), dec.GetBinary()) +} + +func writeRelatedParts( + w *message.Writer, + kr *crypto.KeyRing, + boundary *boundary, + msg liteapi.Message, + atts []liteapi.Attachment, + attData [][]byte, + opts JobOptions, +) error { + hdr := message.Header{} + + hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()}) + + return createPart(w, hdr, func(rel *message.Writer) error { + if err := writeTextPart(rel, kr, msg, opts); err != nil { + return err + } + + for i, att := range atts { + if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil { + return err + } + } + + return nil + }) +} + +func buildPGPRFC822(kr *crypto.KeyRing, msg liteapi.Message, opts JobOptions) ([]byte, error) { + dec, err := msg.Decrypt(kr) + if err != nil { + if !opts.IgnoreDecryptionErrors { + return nil, errors.Wrap(ErrDecryptionFailed, err.Error()) + } + + return buildPGPMIMEFallbackRFC822(msg, opts) + } + + hdr := getMessageHeader(msg, opts) + + sigs, err := msg.ExtractSignatures(kr) + if err != nil { + log.WithError(err).WithField("id", msg.ID).Warn("Extract signature failed") + } + + if len(sigs) > 0 { + return writeMultipartSignedRFC822(hdr, dec, sigs[0]) + } + + return writeMultipartEncryptedRFC822(hdr, dec) +} + +func buildPGPMIMEFallbackRFC822(msg liteapi.Message, opts JobOptions) ([]byte, error) { + hdr := getMessageHeader(msg, opts) + + hdr.SetContentType("multipart/encrypted", map[string]string{ + "boundary": newBoundary(msg.ID).gen(), + "protocol": "application/pgp-encrypted", + }) + + buf := new(bytes.Buffer) + + w, err := message.CreateWriter(buf, hdr) + if err != nil { + return nil, err + } + + var encHdr message.Header + + encHdr.SetContentType("application/pgp-encrypted", nil) + encHdr.Set("Content-Description", "PGP/MIME version identification") + + if err := writePart(w, encHdr, []byte("Version: 1")); err != nil { + return nil, err + } + + var dataHdr message.Header + + dataHdr.SetContentType("application/octet-stream", map[string]string{"name": "encrypted.asc"}) + dataHdr.SetContentDisposition("inline", map[string]string{"filename": "encrypted.asc"}) + dataHdr.Set("Content-Description", "OpenPGP encrypted message") + + if err := writePart(w, dataHdr, []byte(msg.Body)); err != nil { + return nil, err + } + + if err := w.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func writeMultipartSignedRFC822(header message.Header, body []byte, sig liteapi.Signature) ([]byte, error) { //nolint:funlen + buf := new(bytes.Buffer) + + boundary := newBoundary("").gen() + + header.SetContentType("multipart/signed", map[string]string{ + "micalg": sig.Hash, + "protocol": "application/pgp-signature", + "boundary": boundary, + }) + + if err := textproto.WriteHeader(buf, header.Header); err != nil { + return nil, err + } + + mw := textproto.NewMultipartWriter(buf) + + if err := mw.SetBoundary(boundary); err != nil { + return nil, err + } + + bodyHeader, bodyData, err := readHeaderBody(body) + if err != nil { + return nil, err + } + + bodyPart, err := mw.CreatePart(*bodyHeader) + if err != nil { + return nil, err + } + + if _, err := bodyPart.Write(bodyData); err != nil { + return nil, err + } + + var sigHeader message.Header + + sigHeader.SetContentType("application/pgp-signature", map[string]string{"name": "OpenPGP_signature.asc"}) + sigHeader.SetContentDisposition("attachment", map[string]string{"filename": "OpenPGP_signature"}) + sigHeader.Set("Content-Description", "OpenPGP digital signature") + + sigPart, err := mw.CreatePart(sigHeader.Header) + if err != nil { + return nil, err + } + + sigData, err := sig.Data.GetArmored() + if err != nil { + return nil, err + } + + if _, err := sigPart.Write([]byte(sigData)); err != nil { + return nil, err + } + + if err := mw.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func writeMultipartEncryptedRFC822(header message.Header, body []byte) ([]byte, error) { + buf := new(bytes.Buffer) + + bodyHeader, bodyData, err := readHeaderBody(body) + if err != nil { + return nil, err + } + + // If parsed header is empty then either it is malformed or it is missing. + // Anyway message could not be considered multipart/mixed anymore since there will be no boundary. + if bodyHeader.Len() == 0 { + header.Del("Content-Type") + } + + entFields := bodyHeader.Fields() + + for entFields.Next() { + header.Set(entFields.Key(), entFields.Value()) + } + + if err := textproto.WriteHeader(buf, header.Header); err != nil { + return nil, err + } + + if _, err := buf.Write(bodyData); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func getMessageHeader(msg liteapi.Message, opts JobOptions) message.Header { //nolint:funlen + hdr := toMessageHeader(msg.ParsedHeaders) + + // SetText will RFC2047-encode. + if msg.Subject != "" { + hdr.SetText("Subject", msg.Subject) + } + + // mail.Address.String() will RFC2047-encode if necessary. + if msg.Sender != nil { + hdr.Set("From", msg.Sender.String()) + } + + if len(msg.ReplyTos) > 0 { + hdr.Set("Reply-To", toAddressList(msg.ReplyTos)) + } + + if len(msg.ToList) > 0 { + hdr.Set("To", toAddressList(msg.ToList)) + } + + if len(msg.CCList) > 0 { + hdr.Set("Cc", toAddressList(msg.CCList)) + } + + if len(msg.BCCList) > 0 { + hdr.Set("Bcc", toAddressList(msg.BCCList)) + } + + setMessageIDIfNeeded(msg, &hdr) + + // Sanitize the date; it needs to have a valid unix timestamp. + if opts.SanitizeDate { + if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) { + msgDate := SanitizeMessageDate(msg.Time) + hdr.Set("Date", msgDate.In(time.UTC).Format(time.RFC1123Z)) + // We clobbered the date so we save it under X-Original-Date. + hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z)) + } + } + + // Set our internal ID if requested. + // This is important for us to detect whether APPENDed things are actually "move like outlook". + if opts.AddInternalID { + hdr.Set("X-Pm-Internal-Id", msg.ID) + } + + // Set our external ID if requested. + // This was useful during debugging of applemail recovered messages; doesn't help with any behaviour. + if opts.AddExternalID { + hdr.Set("X-Pm-External-Id", "<"+msg.ExternalID+">") + } + + // Set our server date if requested. + // Can be useful to see how long it took for a message to arrive. + if opts.AddMessageDate { + hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z)) + } + + // Include the message ID in the references (supposedly this somehow improves outlook support...). + if opts.AddMessageIDReference { + if references := hdr.Get("References"); !strings.Contains(references, msg.ID) { + hdr.Set("References", references+" <"+msg.ID+"@"+InternalIDDomain+">") + } + } + + return hdr +} + +// SanitizeMessageDate will return time from msgTime timestamp. If timestamp is +// not after epoch the RFC822 publish day will be used. No message should +// realistically be older than RFC822 itself. +func SanitizeMessageDate(msgTime int64) time.Time { + if msgTime := time.Unix(msgTime, 0); msgTime.After(time.Unix(0, 0)) { + return msgTime + } + return time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC) +} + +// setMessageIDIfNeeded sets Message-Id from ExternalID or ID if it's not +// already set. +func setMessageIDIfNeeded(msg liteapi.Message, hdr *message.Header) { + if hdr.Get("Message-Id") == "" { + if msg.ExternalID != "" { + hdr.Set("Message-Id", "<"+msg.ExternalID+">") + } else { + hdr.Set("Message-Id", "<"+msg.ID+"@"+InternalIDDomain+">") + } } } + +func getTextPartHeader(hdr message.Header, body []byte, mimeType rfc822.MIMEType) message.Header { + params := make(map[string]string) + + if utf8.Valid(body) { + params["charset"] = "utf-8" + } + + hdr.SetContentType(string(mimeType), params) + + // Use quoted-printable for all text/... parts + hdr.Set("Content-Transfer-Encoding", "quoted-printable") + + return hdr +} + +func getAttachmentPartHeader(att liteapi.Attachment) message.Header { + hdr := toMessageHeader(liteapi.Headers(att.Headers)) + + // All attachments have a content type. + hdr.SetContentType(string(att.MIMEType), map[string]string{"name": mime.QEncoding.Encode("utf-8", att.Name)}) + + // All attachments have a content disposition. + hdr.SetContentDisposition(string(att.Disposition), map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)}) + + // Use base64 for all attachments except embedded RFC822 messages. + if att.MIMEType != rfc822.MessageRFC822 { + hdr.Set("Content-Transfer-Encoding", "base64") + } else { + hdr.Del("Content-Transfer-Encoding") + } + + return hdr +} + +func toMessageHeader(hdr liteapi.Headers) message.Header { + var res message.Header + + for key, val := range hdr { + for _, val := range val { + // Using AddRaw instead of Add to save key-value pair as byte buffer within Header. + // This buffer is used latter on in message writer to construct message and avoid crash + // when key length is more than 76 characters long. + res.AddRaw([]byte(key + ": " + val + "\r\n")) + } + } + + return res +} + +func toAddressList(addrs []*mail.Address) string { + res := make([]string, len(addrs)) + + for i, addr := range addrs { + res[i] = addr.String() + } + + return strings.Join(res, ", ") +} + +func createPart(w *message.Writer, hdr message.Header, fn func(*message.Writer) error) error { + part, err := w.CreatePart(hdr) + if err != nil { + return err + } + + if err := fn(part); err != nil { + return err + } + + return part.Close() +} + +func writePart(w *message.Writer, hdr message.Header, body []byte) error { + return createPart(w, hdr, func(part *message.Writer) error { + if _, err := part.Write(body); err != nil { + return errors.Wrap(err, "failed to write part body") + } + + return nil + }) +} + +type boundary struct { + val string +} + +func newBoundary(seed string) *boundary { + return &boundary{val: seed} +} + +func (bw *boundary) gen() string { + bw.val = algo.HashHexSHA256(bw.val) + return bw.val +} diff --git a/pkg/message/build_boundary.go b/pkg/message/build_boundary.go deleted file mode 100644 index 6ce3c01e..00000000 --- a/pkg/message/build_boundary.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "github.com/ProtonMail/proton-bridge/v2/pkg/algo" -) - -type boundary struct { - val string -} - -func newBoundary(seed string) *boundary { - return &boundary{val: seed} -} - -func (bw *boundary) gen() string { - bw.val = algo.HashHexSHA256(bw.val) - return bw.val -} diff --git a/pkg/message/build_rfc822_custom.go b/pkg/message/build_custom.go similarity index 91% rename from pkg/message/build_rfc822_custom.go rename to pkg/message/build_custom.go index 48d8a6e0..79e693f2 100644 --- a/pkg/message/build_rfc822_custom.go +++ b/pkg/message/build_custom.go @@ -23,14 +23,14 @@ import ( "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" "github.com/emersion/go-message" + "gitlab.protontech.ch/go/liteapi" ) // writeCustomTextPart writes an armored-PGP text part for a message body that couldn't be decrypted. func writeCustomTextPart( w *message.Writer, - msg *pmapi.Message, + msg liteapi.Message, decError error, ) error { enc, err := crypto.NewPGPMessageFromArmored(msg.Body) @@ -48,7 +48,7 @@ func writeCustomTextPart( var hdr message.Header - hdr.SetContentType(msg.MIMEType, nil) + hdr.SetContentType(string(msg.MIMEType), nil) part, err := w.CreatePart(hdr) if err != nil { @@ -65,7 +65,7 @@ func writeCustomTextPart( // writeCustomAttachmentPart writes an armored-PGP data part for an attachment that couldn't be decrypted. func writeCustomAttachmentPart( w *message.Writer, - att *pmapi.Attachment, + att liteapi.Attachment, msg *crypto.PGPMessage, decError error, ) error { @@ -82,7 +82,7 @@ func writeCustomAttachmentPart( var hdr message.Header hdr.SetContentType("application/octet-stream", map[string]string{"name": filename}) - hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": filename}) + hdr.SetContentDisposition(string(att.Disposition), map[string]string{"filename": filename}) part, err := w.CreatePart(hdr) if err != nil { diff --git a/pkg/message/build_encrypted.go b/pkg/message/build_encrypted.go deleted file mode 100644 index 7d3c8fa4..00000000 --- a/pkg/message/build_encrypted.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "bytes" - "encoding/base64" - "io" - "mime" - "mime/multipart" - "net/http" - "net/textproto" - "strings" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/emersion/go-message" - "github.com/emersion/go-textwrapper" -) - -// BuildEncrypted is used for importing encrypted message. -func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ([]byte, error) { //nolint:funlen - b := &bytes.Buffer{} - boundary := newBoundary(m.ID).gen() - - // Overwrite content for main header for import. - // Even if message has just simple body we should upload as multipart/mixed. - // Each part has encrypted body and header reflects the original header. - mainHeader := convertGoMessageToTextprotoHeader(getMessageHeader(m, JobOptions{})) - mainHeader.Set("Content-Type", "multipart/mixed; boundary="+boundary) - mainHeader.Del("Content-Disposition") - mainHeader.Del("Content-Transfer-Encoding") - if err := WriteHeader(b, mainHeader); err != nil { - return nil, err - } - mw := multipart.NewWriter(b) - if err := mw.SetBoundary(boundary); err != nil { - return nil, err - } - - // Write the body part. - bodyHeader := make(textproto.MIMEHeader) - bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8") - bodyHeader.Set("Content-Disposition", pmapi.DispositionInline) - bodyHeader.Set("Content-Transfer-Encoding", "7bit") - - p, err := mw.CreatePart(bodyHeader) - if err != nil { - return nil, err - } - // First, encrypt the message body. - if err := m.Encrypt(kr, kr); err != nil { - return nil, err - } - if _, err := io.WriteString(p, m.Body); err != nil { - return nil, err - } - - // Write the attachments parts. - for i := 0; i < len(m.Attachments); i++ { - att := m.Attachments[i] - r := readers[i] - h := getAttachmentHeader(att, false) - p, err := mw.CreatePart(h) - if err != nil { - return nil, err - } - - data, err := io.ReadAll(r) - if err != nil { - return nil, err - } - - // Create encrypted writer. - pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil) - if err != nil { - return nil, err - } - - ww := textwrapper.NewRFC822(p) - bw := base64.NewEncoder(base64.StdEncoding, ww) - if _, err := bw.Write(pgpMessage.GetBinary()); err != nil { - return nil, err - } - if err := bw.Close(); err != nil { - return nil, err - } - } - - if err := mw.Close(); err != nil { - return nil, err - } - - return b.Bytes(), nil -} - -func convertGoMessageToTextprotoHeader(h message.Header) textproto.MIMEHeader { - out := make(textproto.MIMEHeader) - hf := h.Fields() - for hf.Next() { - // go-message fields are in the reverse order. - // textproto.MIMEHeader is not ordered except for the values of - // the same key which are ordered - key := textproto.CanonicalMIMEHeaderKey(hf.Key()) - out[key] = append([]string{hf.Value()}, out[key]...) - } - return out -} - -func getAttachmentHeader(att *pmapi.Attachment, buildForIMAP bool) textproto.MIMEHeader { - mediaType := att.MIMEType - if mediaType == "application/pgp-encrypted" { - mediaType = "application/octet-stream" - } - - transferEncoding := "base64" - if mediaType == rfc822Message && buildForIMAP { - transferEncoding = "8bit" - } - - encodedName := pmmime.EncodeHeader(att.Name) - disposition := "attachment" //nolint:goconst - if strings.Contains(att.Header.Get("Content-Disposition"), pmapi.DispositionInline) { - disposition = pmapi.DispositionInline - } - - h := make(textproto.MIMEHeader) - h.Set("Content-Type", mime.FormatMediaType(mediaType, map[string]string{"name": encodedName})) - if transferEncoding != "" { - h.Set("Content-Transfer-Encoding", transferEncoding) - } - h.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{"filename": encodedName})) - - // Forward some original header lines. - forward := []string{"Content-Id", "Content-Description", "Content-Location"} - for _, k := range forward { - v := att.Header.Get(k) - if v != "" { - h.Set(k, v) - } - } - - return h -} - -func WriteHeader(w io.Writer, h textproto.MIMEHeader) (err error) { - if err = http.Header(h).Write(w); err != nil { - return - } - _, err = io.WriteString(w, "\r\n") - return -} diff --git a/pkg/message/build_framework_test.go b/pkg/message/build_framework_test.go index 5a018ec7..b9e9f597 100644 --- a/pkg/message/build_framework_test.go +++ b/pkg/message/build_framework_test.go @@ -21,46 +21,24 @@ import ( "bufio" "bytes" "encoding/base64" - "io" "strings" "testing" "time" + "github.com/ProtonMail/gluon/rfc822" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/message/mocks" "github.com/ProtonMail/proton-bridge/v2/pkg/message/parser" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gitlab.protontech.ch/go/liteapi" "golang.org/x/text/encoding/htmlindex" ) -func newTestFetcher( - m *gomock.Controller, - kr *crypto.KeyRing, - msg *pmapi.Message, - attData ...[]byte, -) Fetcher { - f := mocks.NewMockFetcher(m) - - f.EXPECT().GetMessage(gomock.Any(), msg.ID).Return(msg, nil) - - for i, att := range msg.Attachments { - f.EXPECT().GetAttachment(gomock.Any(), att.ID).Return(newTestReadCloser(attData[i]), nil) - } - - f.EXPECT().KeyRingForAddressID(msg.AddressID).Return(kr, nil) - - return f -} - func newTestMessage( t *testing.T, kr *crypto.KeyRing, messageID, addressID, mimeType, body string, //nolint:unparam date time.Time, -) *pmapi.Message { +) liteapi.Message { enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), kr) require.NoError(t, err) @@ -70,57 +48,47 @@ func newTestMessage( return newRawTestMessage(messageID, addressID, mimeType, arm, date) } -func newRawTestMessage(messageID, addressID, mimeType, body string, date time.Time) *pmapi.Message { - return &pmapi.Message{ - ID: messageID, - AddressID: addressID, - MIMEType: mimeType, - Header: map[string][]string{ +func newRawTestMessage(messageID, addressID, mimeType, body string, date time.Time) liteapi.Message { + return liteapi.Message{ + MessageMetadata: liteapi.MessageMetadata{ + ID: messageID, + AddressID: addressID, + Time: date.Unix(), + }, + ParsedHeaders: liteapi.Headers{ "Content-Type": {mimeType}, "Date": {date.In(time.UTC).Format(time.RFC1123Z)}, }, - Body: body, - Time: date.Unix(), + MIMEType: rfc822.MIMEType(mimeType), + Body: body, } } func addTestAttachment( t *testing.T, kr *crypto.KeyRing, - msg *pmapi.Message, + msg *liteapi.Message, attachmentID, name, mimeType, disposition, data string, ) []byte { enc, err := kr.EncryptAttachment(crypto.NewPlainMessageFromString(data), attachmentID+".bin") require.NoError(t, err) - msg.Attachments = append(msg.Attachments, &pmapi.Attachment{ + msg.Attachments = append(msg.Attachments, liteapi.Attachment{ ID: attachmentID, Name: name, - MIMEType: mimeType, - Header: map[string][]string{ + MIMEType: rfc822.MIMEType(mimeType), + Headers: liteapi.Headers{ "Content-Type": {mimeType}, "Content-Disposition": {disposition}, "Content-Transfer-Encoding": {"base64"}, }, - Disposition: disposition, + Disposition: liteapi.Disposition(disposition), KeyPackets: base64.StdEncoding.EncodeToString(enc.GetBinaryKeyPacket()), }) return enc.GetBinaryDataPacket() } -type testReadCloser struct { - io.Reader -} - -func newTestReadCloser(b []byte) *testReadCloser { - return &testReadCloser{Reader: bytes.NewReader(b)} -} - -func (testReadCloser) Close() error { - return nil -} - type testSection struct { t *testing.T part *parser.Part @@ -130,21 +98,18 @@ type testSection struct { // NOTE: Each section is parsed individually --> cleaner test code but slower... improve this one day? func section(t *testing.T, b []byte, section ...int) *testSection { p, err := parser.New(bytes.NewReader(b)) - assert.NoError(t, err) + require.NoError(t, err) part, err := p.Section(section) require.NoError(t, err) - bs, err := NewBodyStructure(bytes.NewReader(b)) - require.NoError(t, err) - - raw, err := bs.GetSection(bytes.NewReader(b), section) + s, err := rfc822.Parse(b).Part(section...) require.NoError(t, err) return &testSection{ t: t, part: part, - raw: raw, + raw: s.Literal(), } } @@ -249,7 +214,7 @@ type isMatcher struct { } func (matcher isMatcher) match(t *testing.T, have string) { - assert.Equal(t, matcher.want, have) + require.Equal(t, matcher.want, have) } func is(want string) isMatcher { @@ -265,7 +230,7 @@ type isNotMatcher struct { } func (matcher isNotMatcher) match(t *testing.T, have string) { - assert.NotEqual(t, matcher.notWant, have) + require.NotEqual(t, matcher.notWant, have) } func isNot(notWant string) isNotMatcher { @@ -277,7 +242,7 @@ type containsMatcher struct { } func (matcher containsMatcher) match(t *testing.T, have string) { - assert.Contains(t, have, matcher.contains) + require.Contains(t, have, matcher.contains) } func contains(contains string) containsMatcher { @@ -296,7 +261,7 @@ func (matcher decryptsToMatcher) match(t *testing.T, have string) { dec, err := matcher.kr.Decrypt(haveMsg, nil, crypto.GetUnixTime()) require.NoError(t, err) - assert.Equal(t, matcher.want, string(dec.GetBinary())) + require.Equal(t, matcher.want, string(dec.GetBinary())) } func decryptsTo(kr *crypto.KeyRing, want string) decryptsToMatcher { @@ -315,7 +280,7 @@ func (matcher decodesToMatcher) match(t *testing.T, have string) { dec, err := enc.NewDecoder().String(have) require.NoError(t, err) - assert.Equal(t, matcher.want, dec) + require.Equal(t, matcher.want, dec) } func decodesTo(charset string, want string) decodesToMatcher { @@ -328,8 +293,8 @@ type verifiesAgainstMatcher struct { } func (matcher verifiesAgainstMatcher) match(t *testing.T, have string) { - assert.NoError(t, matcher.kr.VerifyDetached( - crypto.NewPlainMessage(bytes.TrimSuffix([]byte(have), []byte("\r\n"))), + require.NoError(t, matcher.kr.VerifyDetached( + crypto.NewPlainMessage([]byte(have)), matcher.sig, crypto.GetUnixTime()), ) @@ -347,7 +312,7 @@ func (matcher maxLineLengthMatcher) match(t *testing.T, have string) { scanner := bufio.NewScanner(strings.NewReader(have)) for scanner.Scan() { - assert.Less(t, len(scanner.Text()), matcher.wantMax) + require.Less(t, len(scanner.Text()), matcher.wantMax) } } diff --git a/pkg/message/build_rfc822.go b/pkg/message/build_rfc822.go deleted file mode 100644 index eb7e0b9a..00000000 --- a/pkg/message/build_rfc822.go +++ /dev/null @@ -1,544 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "bytes" - "encoding/base64" - "mime" - "net/mail" - "strings" - "time" - "unicode/utf8" - - "github.com/ProtonMail/go-rfc5322" - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/emersion/go-message" - "github.com/emersion/go-message/textproto" - "github.com/pkg/errors" -) - -func buildRFC822(kr *crypto.KeyRing, msg *pmapi.Message, attData map[string][]byte, opts JobOptions) ([]byte, error) { - switch { - case len(msg.Attachments) > 0: - return buildMultipartRFC822(kr, msg, attData, opts) - - case msg.MIMEType == "multipart/mixed": - return buildPGPRFC822(kr, msg, opts) - - default: - return buildSimpleRFC822(kr, msg, opts) - } -} - -func buildSimpleRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) { - dec, err := msg.Decrypt(kr) - if err != nil { - if !opts.IgnoreDecryptionErrors { - return nil, errors.Wrap(ErrDecryptionFailed, err.Error()) - } - - return buildMultipartRFC822(kr, msg, nil, opts) - } - - hdr := getTextPartHeader(getMessageHeader(msg, opts), dec, msg.MIMEType) - - buf := new(bytes.Buffer) - - w, err := message.CreateWriter(buf, hdr) - if err != nil { - return nil, err - } - - if _, err := w.Write(dec); err != nil { - return nil, err - } - - if err := w.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func buildMultipartRFC822( - kr *crypto.KeyRing, - msg *pmapi.Message, - attData map[string][]byte, - opts JobOptions, -) ([]byte, error) { - boundary := newBoundary(msg.ID) - - hdr := getMessageHeader(msg, opts) - - hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()}) - - buf := new(bytes.Buffer) - - w, err := message.CreateWriter(buf, hdr) - if err != nil { - return nil, err - } - - var ( - inlineAtts []*pmapi.Attachment - inlineData [][]byte - attachAtts []*pmapi.Attachment - attachData [][]byte - ) - - for _, att := range msg.Attachments { - if att.Disposition == pmapi.DispositionInline { - inlineAtts = append(inlineAtts, att) - inlineData = append(inlineData, attData[att.ID]) - } else { - attachAtts = append(attachAtts, att) - attachData = append(attachData, attData[att.ID]) - } - } - - if len(inlineAtts) > 0 { - if err := writeRelatedParts(w, kr, boundary, msg, inlineAtts, inlineData, opts); err != nil { - return nil, err - } - } else if err := writeTextPart(w, kr, msg, opts); err != nil { - return nil, err - } - - for i, att := range attachAtts { - if err := writeAttachmentPart(w, kr, att, attachData[i], opts); err != nil { - return nil, err - } - } - - if err := w.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func writeTextPart( - w *message.Writer, - kr *crypto.KeyRing, - msg *pmapi.Message, - opts JobOptions, -) error { - dec, err := msg.Decrypt(kr) - if err != nil { - if !opts.IgnoreDecryptionErrors { - return errors.Wrap(ErrDecryptionFailed, err.Error()) - } - - return writeCustomTextPart(w, msg, err) - } - - return writePart(w, getTextPartHeader(message.Header{}, dec, msg.MIMEType), dec) -} - -func writeAttachmentPart( - w *message.Writer, - kr *crypto.KeyRing, - att *pmapi.Attachment, - attData []byte, - opts JobOptions, -) error { - kps, err := base64.StdEncoding.DecodeString(att.KeyPackets) - if err != nil { - return err - } - - msg := crypto.NewPGPSplitMessage(kps, attData).GetPGPMessage() - - dec, err := kr.Decrypt(msg, nil, crypto.GetUnixTime()) - if err != nil { - if !opts.IgnoreDecryptionErrors { - return errors.Wrap(ErrDecryptionFailed, err.Error()) - } - - log. - WithField("attID", att.ID). - WithField("msgID", att.MessageID). - WithError(err). - Warn("Attachment decryption failed") - - return writeCustomAttachmentPart(w, att, msg, err) - } - - return writePart(w, getAttachmentPartHeader(att), dec.GetBinary()) -} - -func writeRelatedParts( - w *message.Writer, - kr *crypto.KeyRing, - boundary *boundary, - msg *pmapi.Message, - atts []*pmapi.Attachment, - attData [][]byte, - opts JobOptions, -) error { - hdr := message.Header{} - - hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()}) - - return createPart(w, hdr, func(rel *message.Writer) error { - if err := writeTextPart(rel, kr, msg, opts); err != nil { - return err - } - - for i, att := range atts { - if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil { - return err - } - } - - return nil - }) -} - -func buildPGPRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) { - dec, err := msg.Decrypt(kr) - if err != nil { - if !opts.IgnoreDecryptionErrors { - return nil, errors.Wrap(ErrDecryptionFailed, err.Error()) - } - - return buildPGPMIMEFallbackRFC822(msg, opts) - } - - hdr := getMessageHeader(msg, opts) - - sigs, err := msg.ExtractSignatures(kr) - if err != nil { - log.WithError(err).WithField("id", msg.ID).Warn("Extract signature failed") - } - - if len(sigs) > 0 { - return writeMultipartSignedRFC822(hdr, dec, sigs[0]) - } - - return writeMultipartEncryptedRFC822(hdr, dec) -} - -func buildPGPMIMEFallbackRFC822(msg *pmapi.Message, opts JobOptions) ([]byte, error) { - hdr := getMessageHeader(msg, opts) - - hdr.SetContentType("multipart/encrypted", map[string]string{ - "boundary": newBoundary(msg.ID).gen(), - "protocol": "application/pgp-encrypted", - }) - - buf := new(bytes.Buffer) - - w, err := message.CreateWriter(buf, hdr) - if err != nil { - return nil, err - } - - var encHdr message.Header - - encHdr.SetContentType("application/pgp-encrypted", nil) - encHdr.Set("Content-Description", "PGP/MIME version identification") - - if err := writePart(w, encHdr, []byte("Version: 1")); err != nil { - return nil, err - } - - var dataHdr message.Header - - dataHdr.SetContentType("application/octet-stream", map[string]string{"name": "encrypted.asc"}) - dataHdr.SetContentDisposition("inline", map[string]string{"filename": "encrypted.asc"}) - dataHdr.Set("Content-Description", "OpenPGP encrypted message") - - if err := writePart(w, dataHdr, []byte(msg.Body)); err != nil { - return nil, err - } - - if err := w.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func writeMultipartSignedRFC822(header message.Header, body []byte, sig pmapi.Signature) ([]byte, error) { //nolint:funlen - buf := new(bytes.Buffer) - - boundary := newBoundary("").gen() - - header.SetContentType("multipart/signed", map[string]string{ - "micalg": sig.Hash, - "protocol": "application/pgp-signature", - "boundary": boundary, - }) - - if err := textproto.WriteHeader(buf, header.Header); err != nil { - return nil, err - } - - mw := textproto.NewMultipartWriter(buf) - - if err := mw.SetBoundary(boundary); err != nil { - return nil, err - } - - bodyHeader, bodyData, err := readHeaderBody(body) - if err != nil { - return nil, err - } - - bodyPart, err := mw.CreatePart(*bodyHeader) - if err != nil { - return nil, err - } - - if _, err := bodyPart.Write(bodyData); err != nil { - return nil, err - } - - var sigHeader message.Header - - sigHeader.SetContentType("application/pgp-signature", map[string]string{"name": "OpenPGP_signature.asc"}) - sigHeader.SetContentDisposition("attachment", map[string]string{"filename": "OpenPGP_signature"}) - sigHeader.Set("Content-Description", "OpenPGP digital signature") - - sigPart, err := mw.CreatePart(sigHeader.Header) - if err != nil { - return nil, err - } - - sigData, err := crypto.NewPGPSignature(sig.Data).GetArmored() - if err != nil { - return nil, err - } - - if _, err := sigPart.Write([]byte(sigData)); err != nil { - return nil, err - } - - if err := mw.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func writeMultipartEncryptedRFC822(header message.Header, body []byte) ([]byte, error) { - buf := new(bytes.Buffer) - - bodyHeader, bodyData, err := readHeaderBody(body) - if err != nil { - return nil, err - } - - // If parsed header is empty then either it is malformed or it is missing. - // Anyway message could not be considered multipart/mixed anymore since there will be no boundary. - if bodyHeader.Len() == 0 { - header.Del("Content-Type") - } - - entFields := bodyHeader.Fields() - - for entFields.Next() { - header.Set(entFields.Key(), entFields.Value()) - } - - if err := textproto.WriteHeader(buf, header.Header); err != nil { - return nil, err - } - - if _, err := buf.Write(bodyData); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func getMessageHeader(msg *pmapi.Message, opts JobOptions) message.Header { //nolint:funlen - hdr := toMessageHeader(msg.Header) - - // SetText will RFC2047-encode. - if msg.Subject != "" { - hdr.SetText("Subject", msg.Subject) - } - - // mail.Address.String() will RFC2047-encode if necessary. - if msg.Sender != nil { - hdr.Set("From", msg.Sender.String()) - } - - if len(msg.ReplyTos) > 0 { - hdr.Set("Reply-To", toAddressList(msg.ReplyTos)) - } - - if len(msg.ToList) > 0 { - hdr.Set("To", toAddressList(msg.ToList)) - } - - if len(msg.CCList) > 0 { - hdr.Set("Cc", toAddressList(msg.CCList)) - } - - if len(msg.BCCList) > 0 { - hdr.Set("Bcc", toAddressList(msg.BCCList)) - } - - setMessageIDIfNeeded(msg, &hdr) - - // Sanitize the date; it needs to have a valid unix timestamp. - if opts.SanitizeDate { - if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) { - msgDate := SanitizeMessageDate(msg.Time) - hdr.Set("Date", msgDate.In(time.UTC).Format(time.RFC1123Z)) - // We clobbered the date so we save it under X-Original-Date. - hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z)) - } - } - - // Set our internal ID if requested. - // This is important for us to detect whether APPENDed things are actually "move like outlook". - if opts.AddInternalID { - hdr.Set("X-Pm-Internal-Id", msg.ID) - } - - // Set our external ID if requested. - // This was useful during debugging of applemail recovered messages; doesn't help with any behaviour. - if opts.AddExternalID { - hdr.Set("X-Pm-External-Id", "<"+msg.ExternalID+">") - } - - // Set our server date if requested. - // Can be useful to see how long it took for a message to arrive. - if opts.AddMessageDate { - hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z)) - } - - // Include the message ID in the references (supposedly this somehow improves outlook support...). - if opts.AddMessageIDReference { - if references := hdr.Get("References"); !strings.Contains(references, msg.ID) { - hdr.Set("References", references+" <"+msg.ID+"@"+pmapi.InternalIDDomain+">") - } - } - - return hdr -} - -// SanitizeMessageDate will return time from msgTime timestamp. If timestamp is -// not after epoch the RFC822 publish day will be used. No message should -// realistically be older than RFC822 itself. -func SanitizeMessageDate(msgTime int64) time.Time { - if msgTime := time.Unix(msgTime, 0); msgTime.After(time.Unix(0, 0)) { - return msgTime - } - return time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC) -} - -// setMessageIDIfNeeded sets Message-Id from ExternalID or ID if it's not -// already set. -func setMessageIDIfNeeded(msg *pmapi.Message, hdr *message.Header) { - if hdr.Get("Message-Id") == "" { - if msg.ExternalID != "" { - hdr.Set("Message-Id", "<"+msg.ExternalID+">") - } else { - hdr.Set("Message-Id", "<"+msg.ID+"@"+pmapi.InternalIDDomain+">") - } - } -} - -func getTextPartHeader(hdr message.Header, body []byte, mimeType string) message.Header { - params := make(map[string]string) - - if utf8.Valid(body) { - params["charset"] = "utf-8" - } - - hdr.SetContentType(mimeType, params) - - // Use quoted-printable for all text/... parts - hdr.Set("Content-Transfer-Encoding", "quoted-printable") - - return hdr -} - -func getAttachmentPartHeader(att *pmapi.Attachment) message.Header { - hdr := toMessageHeader(mail.Header(att.Header)) - - // All attachments have a content type. - hdr.SetContentType(att.MIMEType, map[string]string{"name": mime.QEncoding.Encode("utf-8", att.Name)}) - - // All attachments have a content disposition. - hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)}) - - // Use base64 for all attachments except embedded RFC822 messages. - if att.MIMEType != rfc822Message { - hdr.Set("Content-Transfer-Encoding", "base64") - } else { - hdr.Del("Content-Transfer-Encoding") - } - - return hdr -} - -func toMessageHeader(hdr mail.Header) message.Header { - var res message.Header - - for key, val := range hdr { - for _, val := range val { - // Using AddRaw instead of Add to save key-value pair as byte buffer within Header. - // This buffer is used latter on in message writer to construct message and avoid crash - // when key length is more than 76 characters long. - res.AddRaw([]byte(key + ": " + val + "\r\n")) - } - } - - return res -} - -func toAddressList(addrs []*mail.Address) string { - res := make([]string, len(addrs)) - - for i, addr := range addrs { - res[i] = addr.String() - } - - return strings.Join(res, ", ") -} - -func createPart(w *message.Writer, hdr message.Header, fn func(*message.Writer) error) error { - part, err := w.CreatePart(hdr) - if err != nil { - return err - } - - if err := fn(part); err != nil { - return err - } - - return part.Close() -} - -func writePart(w *message.Writer, hdr message.Header, body []byte) error { - return createPart(w, hdr, func(part *message.Writer) error { - if _, err := part.Write(body); err != nil { - return errors.Wrap(err, "failed to write part body") - } - - return nil - }) -} diff --git a/pkg/message/build_test.go b/pkg/message/build_test.go index e814223c..506d727b 100644 --- a/pkg/message/build_test.go +++ b/pkg/message/build_test.go @@ -18,16 +18,15 @@ package message import ( - "context" - "errors" "net/mail" + "os" + "path/filepath" "strings" "testing" "time" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/pkg/message/mocks" - tests "github.com/ProtonMail/proton-bridge/v2/test" + "github.com/ProtonMail/proton-bridge/v2/utils" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -37,16 +36,10 @@ func TestBuildPlainMessage(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -59,17 +52,11 @@ func TestBuildPlainMessageWithLongKey(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(1, 1) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) - msg.Header["ReallyVeryVeryVeryVeryVeryLongLongLongLongLongLongLongKeyThatWillHaveNotSoLongValue"] = []string{"value"} + msg.ParsedHeaders["ReallyVeryVeryVeryVeryVeryLongLongLongLongLongLongLongKeyThatWillHaveNotSoLongValue"] = []string{"value"} - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -83,16 +70,10 @@ func TestBuildHTMLMessage(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Now()) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -105,18 +86,12 @@ func TestBuildPlainEncryptedMessage(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-plaintext.eml") - body := readerToString(getFileReader("pgp-mime-body-plaintext.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -136,18 +111,12 @@ func TestBuildPlainEncryptedMessageMissingHeader(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(1, 1) - defer b.Done() + body := readFile(t, "plaintext-missing-header.eml") - body := readerToString(getFileReader("plaintext-missing-header.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Now()) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -159,18 +128,12 @@ func TestBuildPlainEncryptedMessageInvalidHeader(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(1, 1) - defer b.Done() + body := readFile(t, "plaintext-invalid-header.eml") - body := readerToString(getFileReader("plaintext-invalid-header.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Now()) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -182,13 +145,10 @@ func TestBuildPlainSignedEncryptedMessageMissingHeader(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(1, 1) - defer b.Done() + body := readFile(t, "plaintext-missing-header.eml") - body := readerToString(getFileReader("plaintext-missing-header.eml")) - - kr := tests.MakeKeyRing(t) - sig := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) + sig := utils.MakeKeyRing(t) enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig) require.NoError(t, err) @@ -198,10 +158,7 @@ func TestBuildPlainSignedEncryptedMessageMissingHeader(t *testing.T) { msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -225,13 +182,10 @@ func TestBuildPlainSignedEncryptedMessageInvalidHeader(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(1, 1) - defer b.Done() + body := readFile(t, "plaintext-invalid-header.eml") - body := readerToString(getFileReader("plaintext-invalid-header.eml")) - - kr := tests.MakeKeyRing(t) - sig := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) + sig := utils.MakeKeyRing(t) enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig) require.NoError(t, err) @@ -241,10 +195,7 @@ func TestBuildPlainSignedEncryptedMessageInvalidHeader(t *testing.T) { msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -268,18 +219,12 @@ func TestBuildPlainEncryptedLatin2Message(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-plaintext-latin2.eml") - body := readerToString(getFileReader("pgp-mime-body-plaintext-latin2.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -296,18 +241,12 @@ func TestBuildHTMLEncryptedMessage(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-html.eml") - body := readerToString(getFileReader("pgp-mime-body-html.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -328,13 +267,10 @@ func TestBuildPlainSignedMessage(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "text_plain.eml") - body := readerToString(getFileReader("text_plain.eml")) - - kr := tests.MakeKeyRing(t) - sig := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) + sig := utils.MakeKeyRing(t) enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig) require.NoError(t, err) @@ -344,10 +280,7 @@ func TestBuildPlainSignedMessage(t *testing.T) { msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -372,13 +305,10 @@ func TestBuildPlainSignedBase64Message(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "text_plain_base64.eml") - body := readerToString(getFileReader("text_plain_base64.eml")) - - kr := tests.MakeKeyRing(t) - sig := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) + sig := utils.MakeKeyRing(t) enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig) require.NoError(t, err) @@ -388,10 +318,7 @@ func TestBuildPlainSignedBase64Message(t *testing.T) { msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -417,18 +344,12 @@ func TestBuildSignedPlainEncryptedMessage(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-signed-plaintext.eml") - body := readerToString(getFileReader("pgp-mime-body-signed-plaintext.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -460,18 +381,12 @@ func TestBuildSignedHTMLEncryptedMessage(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-signed-html.eml") - body := readerToString(getFileReader("pgp-mime-body-signed-html.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -505,18 +420,12 @@ func TestBuildSignedPlainEncryptedMessageWithPubKey(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-signed-plaintext-with-pubkey.eml") - body := readerToString(getFileReader("pgp-mime-body-signed-plaintext-with-pubkey.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -557,18 +466,12 @@ func TestBuildSignedHTMLEncryptedMessageWithPubKey(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-signed-html-with-pubkey.eml") - body := readerToString(getFileReader("pgp-mime-body-signed-html-with-pubkey.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -610,18 +513,12 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T) m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-signed-multipart-alternative-with-pubkey.eml") - body := readerToString(getFileReader("pgp-mime-body-signed-multipart-alternative-with-pubkey.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -679,18 +576,12 @@ func TestBuildSignedEmbeddedMessageRFC822EncryptedMessageWithPubKey(t *testing.T m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + body := readFile(t, "pgp-mime-body-signed-embedded-message-rfc822-with-pubkey.eml") - body := readerToString(getFileReader("pgp-mime-body-signed-embedded-message-rfc822-with-pubkey.eml")) - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -736,17 +627,11 @@ func TestBuildHTMLMessageWithAttachment(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Now()) - att := addTestAttachment(t, kr, msg, "attachID", "file.png", "image/png", "attachment", "attachment") + att := addTestAttachment(t, kr, &msg, "attachID", "file.png", "image/png", "attachment", "attachment") - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, att), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{}) require.NoError(t, err) section(t, res, 1). @@ -766,17 +651,11 @@ func TestBuildHTMLMessageWithRFC822Attachment(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Now()) - att := addTestAttachment(t, kr, msg, "attachID", "file.eml", "message/rfc822", "attachment", "... message/rfc822 ...") + att := addTestAttachment(t, kr, &msg, "attachID", "file.eml", "message/rfc822", "attachment", "... message/rfc822 ...") - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, att), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{}) require.NoError(t, err) section(t, res, 1). @@ -796,17 +675,11 @@ func TestBuildHTMLMessageWithInlineAttachment(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Now()) - inl := addTestAttachment(t, kr, msg, "inlineID", "file.png", "image/png", "inline", "inline") + inl := addTestAttachment(t, kr, &msg, "inlineID", "file.png", "image/png", "inline", "inline") - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, inl), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"inlineID": inl}, JobOptions{}) require.NoError(t, err) section(t, res, 1). @@ -829,20 +702,19 @@ func TestBuildHTMLMessageWithComplexAttachments(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Now()) - inl0 := addTestAttachment(t, kr, msg, "inlineID0", "inline0.png", "image/png", "inline", "inline0") - inl1 := addTestAttachment(t, kr, msg, "inlineID1", "inline1.png", "image/png", "inline", "inline1") - att0 := addTestAttachment(t, kr, msg, "attachID0", "attach0.png", "image/png", "attachment", "attach0") - att1 := addTestAttachment(t, kr, msg, "attachID1", "attach1.png", "image/png", "attachment", "attach1") + inl0 := addTestAttachment(t, kr, &msg, "inlineID0", "inline0.png", "image/png", "inline", "inline0") + inl1 := addTestAttachment(t, kr, &msg, "inlineID1", "inline1.png", "image/png", "inline", "inline1") + att0 := addTestAttachment(t, kr, &msg, "attachID0", "attach0.png", "image/png", "attachment", "attach0") + att1 := addTestAttachment(t, kr, &msg, "attachID1", "attach1.png", "image/png", "attachment", "attach1") - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, inl0, inl1, att0, att1), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{ + "inlineID0": inl0, + "inlineID1": inl1, + "attachID0": att0, + "attachID1": att1, + }, JobOptions{}) require.NoError(t, err) section(t, res, 1). @@ -886,17 +758,11 @@ func TestBuildAttachmentWithExoticFilename(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Now()) - att := addTestAttachment(t, kr, msg, "attachID", `I řeally šhould leařn czech.png`, "image/png", "attachment", "attachment") + att := addTestAttachment(t, kr, &msg, "attachID", `I řeally šhould leařn czech.png`, "image/png", "attachment", "attachment") - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, att), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{}) require.NoError(t, err) // The "name" and "filename" params should actually be RFC2047-encoded because they aren't 7-bit clean. @@ -912,19 +778,13 @@ func TestBuildAttachmentWithLongFilename(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - veryLongName := strings.Repeat("a", 200) + ".png" - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Now()) - att := addTestAttachment(t, kr, msg, "attachID", veryLongName, "image/png", "attachment", "attachment") + att := addTestAttachment(t, kr, &msg, "attachID", veryLongName, "image/png", "attachment", "attachment") - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, att), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{}) require.NoError(t, err) // NOTE: hasMaxLineLength is too high! Long filenames should be linewrapped using multipart filenames. @@ -940,16 +800,10 @@ func TestBuildMessageDate(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res).expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)) @@ -959,35 +813,22 @@ func TestBuildMessageWithInvalidDate(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) // Create a message with "invalid" (according to applemail) date (before unix time 0). msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Unix(-1, 0)) // Build the message as usual; the date will be before 1970. - jobRaw, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - resRaw, err := jobRaw.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) - done() - section(t, resRaw). + section(t, res). expectDate(is(`Wed, 31 Dec 1969 23:59:59 +0000`)). expectHeader(`X-Original-Date`, isMissing()) // Build the message with date sanitization enabled; the date will be RFC822's birthdate. - jobFix, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg), - msg.ID, - JobOptions{SanitizeDate: true}, - ForegroundPriority, - ) - resFix, err := jobFix.GetResult() + resFix, err := BuildRFC822(kr, msg, nil, JobOptions{SanitizeDate: true}) require.NoError(t, err) - done() section(t, resFix). expectDate(is(`Fri, 13 Aug 1982 00:00:00 +0000`)). @@ -998,16 +839,10 @@ func TestBuildMessageInternalID(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res).expectHeader(`Message-Id`, is(``)) @@ -1017,19 +852,13 @@ func TestBuildMessageExternalID(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) // Set the message's external ID; this should be used preferentially to set the Message-Id header field. msg.ExternalID = "externalID" - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res).expectHeader(`Message-Id`, is(``)) @@ -1039,18 +868,12 @@ func TestBuild8BitBody(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) // Set an 8-bit body; the charset should be set to UTF-8. msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "I řeally šhould leařn czech", time.Now()) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res).expectContentTypeParam(`charset`, is(`utf-8`)) @@ -1060,19 +883,13 @@ func TestBuild8BitSubject(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) // Set an 8-bit subject; it should be RFC2047-encoded. msg.Subject = `I řeally šhould leařn czech` - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -1084,10 +901,7 @@ func TestBuild8BitSender(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) // Set an 8-bit sender; it should be RFC2047-encoded. @@ -1096,10 +910,7 @@ func TestBuild8BitSender(t *testing.T) { Address: `mail@example.com`, } - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -1111,10 +922,7 @@ func TestBuild8BitRecipients(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) // Set an 8-bit sender; it should be RFC2047-encoded. @@ -1123,10 +931,7 @@ func TestBuild8BitRecipients(t *testing.T) { {Name: `leařn czech`, Address: `mail2@example.com`}, } - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res). @@ -1138,32 +943,19 @@ func TestBuildIncludeMessageIDReference(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) // Add references. - msg.Header["References"] = []string{""} + msg.ParsedHeaders["References"] = []string{""} - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) - done() section(t, res).expectHeader(`References`, is(``)) - jobRef, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg), - msg.ID, - JobOptions{AddMessageIDReference: true}, - ForegroundPriority, - ) - resRef, err := jobRef.GetResult() + resRef, err := BuildRFC822(kr, msg, nil, JobOptions{AddMessageIDReference: true}) require.NoError(t, err) - done() section(t, resRef).expectHeader(`References`, is(` `)) } @@ -1172,148 +964,59 @@ func TestBuildMessageIsDeterministic(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) - inl := addTestAttachment(t, kr, msg, "inlineID", "file.png", "image/png", "inline", "inline") - att := addTestAttachment(t, kr, msg, "attachID", "attach.png", "image/png", "attachment", "attachment") + inl := addTestAttachment(t, kr, &msg, "inlineID", "file.png", "image/png", "inline", "inline") + att := addTestAttachment(t, kr, &msg, "attachID", "attach.png", "image/png", "attachment", "attachment") - job1, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, inl, att), msg.ID, ForegroundPriority) - res1, err := job1.GetResult() + res1, err := BuildRFC822(kr, msg, map[string][]byte{"inlineID": inl, "attachID": att}, JobOptions{}) require.NoError(t, err) - done() - job2, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, inl, att), msg.ID, ForegroundPriority) - res2, err := job2.GetResult() + res2, err := BuildRFC822(kr, msg, map[string][]byte{"inlineID": inl, "attachID": att}, JobOptions{}) require.NoError(t, err) - done() assert.Equal(t, res1, res2) } -func TestBuildParallel(t *testing.T) { - m := gomock.NewController(t) - defer m.Finish() - - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) - msg1 := newTestMessage(t, kr, "messageID1", "addressID", "text/plain", "body1", time.Now()) - msg2 := newTestMessage(t, kr, "messageID2", "addressID", "text/plain", "body2", time.Now()) - - job1, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg1), msg1.ID, ForegroundPriority) - defer done() - - job2, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg2), msg2.ID, ForegroundPriority) - defer done() - - res1, err := job1.GetResult() - require.NoError(t, err) - - section(t, res1).expectBody(is(`body1`)) - - res2, err := job2.GetResult() - require.NoError(t, err) - - section(t, res2).expectBody(is(`body2`)) -} - -func TestBuildParallelSameMessage(t *testing.T) { - m := gomock.NewController(t) - defer m.Finish() - - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) - msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) - - // Jobs for the same messageID are shared so fetcher is only called once. - fetcher := newTestFetcher(m, kr, msg) - - job1, done := b.NewJob(context.Background(), fetcher, msg.ID, ForegroundPriority) - defer done() - - job2, done := b.NewJob(context.Background(), fetcher, msg.ID, ForegroundPriority) - defer done() - - res1, err := job1.GetResult() - require.NoError(t, err) - - section(t, res1).expectBody(is(`body`)) - - res2, err := job2.GetResult() - require.NoError(t, err) - - section(t, res2).expectBody(is(`body`)) -} - func TestBuildUndecryptableMessage(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) // Use a different keyring for encrypting the message; it won't be decryptable. - msg := newTestMessage(t, tests.MakeKeyRing(t), "messageID", "addressID", "text/plain", "body", time.Now()) + msg := newTestMessage(t, utils.MakeKeyRing(t), "messageID", "addressID", "text/plain", "body", time.Now()) - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID, ForegroundPriority) - defer done() - - _, err := job.GetResult() - assert.True(t, errors.Is(err, ErrDecryptionFailed)) + _, err := BuildRFC822(kr, msg, nil, JobOptions{}) + require.ErrorIs(t, err, ErrDecryptionFailed) } func TestBuildUndecryptableAttachment(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) // Use a different keyring for encrypting the attachment; it won't be decryptable. - att := addTestAttachment(t, tests.MakeKeyRing(t), msg, "attachID", "file.png", "image/png", "attachment", "attachment") + att := addTestAttachment(t, utils.MakeKeyRing(t), &msg, "attachID", "file.png", "image/png", "attachment", "attachment") - job, done := b.NewJob(context.Background(), newTestFetcher(m, kr, msg, att), msg.ID, ForegroundPriority) - defer done() - - _, err := job.GetResult() - assert.True(t, errors.Is(err, ErrDecryptionFailed)) + _, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{}) + require.ErrorIs(t, err, ErrDecryptionFailed) } func TestBuildCustomMessagePlain(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) // Use a different keyring for encrypting the message; it won't be decryptable. - foreignKR := tests.MakeKeyRing(t) + foreignKR := utils.MakeKeyRing(t) msg := newTestMessage(t, foreignKR, "messageID", "addressID", "text/plain", "body", time.Now()) // Tell the job to ignore decryption errors; a custom message will be returned instead of an error. - job, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg), - msg.ID, - JobOptions{IgnoreDecryptionErrors: true}, - ForegroundPriority, - ) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true}) require.NoError(t, err) section(t, res). @@ -1330,26 +1033,14 @@ func TestBuildCustomMessageHTML(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) // Use a different keyring for encrypting the message; it won't be decryptable. - foreignKR := tests.MakeKeyRing(t) + foreignKR := utils.MakeKeyRing(t) msg := newTestMessage(t, foreignKR, "messageID", "addressID", "text/html", "body", time.Now()) // Tell the job to ignore decryption errors; a custom message will be returned instead of an error. - job, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg), - msg.ID, - JobOptions{IgnoreDecryptionErrors: true}, - ForegroundPriority, - ) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true}) require.NoError(t, err) section(t, res). @@ -1366,30 +1057,18 @@ func TestBuildCustomMessageEncrypted(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() + kr := utils.MakeKeyRing(t) - kr := tests.MakeKeyRing(t) - - body := readerToString(getFileReader("pgp-mime-body-plaintext.eml")) + body := readFile(t, "pgp-mime-body-plaintext.eml") // Use a different keyring for encrypting the message; it won't be decryptable. - foreignKR := tests.MakeKeyRing(t) + foreignKR := utils.MakeKeyRing(t) msg := newTestMessage(t, foreignKR, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) msg.Subject = "this is a subject to make sure we preserve subject" // Tell the job to ignore decryption errors; a custom message will be returned instead of an error. - job, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg), - msg.ID, - JobOptions{IgnoreDecryptionErrors: true}, - ForegroundPriority, - ) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true}) require.NoError(t, err) section(t, res). @@ -1415,27 +1094,15 @@ func TestBuildCustomMessagePlainWithAttachment(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) // Use a different keyring for encrypting the message; it won't be decryptable. - foreignKR := tests.MakeKeyRing(t) + foreignKR := utils.MakeKeyRing(t) msg := newTestMessage(t, foreignKR, "messageID", "addressID", "text/plain", "body", time.Now()) - att := addTestAttachment(t, foreignKR, msg, "attachID", "file.png", "image/png", "attachment", "attachment") + att := addTestAttachment(t, foreignKR, &msg, "attachID", "file.png", "image/png", "attachment", "attachment") // Tell the job to ignore decryption errors; a custom message will be returned instead of an error. - job, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg, att), - msg.ID, - JobOptions{IgnoreDecryptionErrors: true}, - ForegroundPriority, - ) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{IgnoreDecryptionErrors: true}) require.NoError(t, err) section(t, res). @@ -1460,27 +1127,15 @@ func TestBuildCustomMessageHTMLWithAttachment(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) // Use a different keyring for encrypting the message; it won't be decryptable. - foreignKR := tests.MakeKeyRing(t) + foreignKR := utils.MakeKeyRing(t) msg := newTestMessage(t, foreignKR, "messageID", "addressID", "text/html", "body", time.Now()) - att := addTestAttachment(t, foreignKR, msg, "attachID", "file.png", "image/png", "attachment", "attachment") + att := addTestAttachment(t, foreignKR, &msg, "attachID", "file.png", "image/png", "attachment", "attachment") // Tell the job to ignore decryption errors; a custom message will be returned instead of an error. - job, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg, att), - msg.ID, - JobOptions{IgnoreDecryptionErrors: true}, - ForegroundPriority, - ) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{IgnoreDecryptionErrors: true}) require.NoError(t, err) section(t, res). @@ -1505,29 +1160,17 @@ func TestBuildCustomMessageOnlyBodyIsUndecryptable(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) // Use a different keyring for encrypting the message; it won't be decryptable. - foreignKR := tests.MakeKeyRing(t) + foreignKR := utils.MakeKeyRing(t) msg := newTestMessage(t, foreignKR, "messageID", "addressID", "text/html", "body", time.Now()) // Use the original keyring for encrypting the attachment; it should decrypt fine. - att := addTestAttachment(t, kr, msg, "attachID", "file.png", "image/png", "attachment", "attachment") + att := addTestAttachment(t, kr, &msg, "attachID", "file.png", "image/png", "attachment", "attachment") // Tell the job to ignore decryption errors; a custom message will be returned instead of an error. - job, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg, att), - msg.ID, - JobOptions{IgnoreDecryptionErrors: true}, - ForegroundPriority, - ) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{IgnoreDecryptionErrors: true}) require.NoError(t, err) section(t, res). @@ -1551,28 +1194,16 @@ func TestBuildCustomMessageOnlyAttachmentIsUndecryptable(t *testing.T) { m := gomock.NewController(t) defer m.Finish() - b := NewBuilder(2, 2) - defer b.Done() - // Use the original keyring for encrypting the message; it should decrypt fine. - kr := tests.MakeKeyRing(t) + kr := utils.MakeKeyRing(t) msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "body", time.Now()) // Use a different keyring for encrypting the attachment; it won't be decryptable. - foreignKR := tests.MakeKeyRing(t) - att := addTestAttachment(t, foreignKR, msg, "attachID", "file.png", "image/png", "attachment", "attachment") + foreignKR := utils.MakeKeyRing(t) + att := addTestAttachment(t, foreignKR, &msg, "attachID", "file.png", "image/png", "attachment", "attachment") // Tell the job to ignore decryption errors; a custom message will be returned instead of an error. - job, done := b.NewJobWithOptions( - context.Background(), - newTestFetcher(m, kr, msg, att), - msg.ID, - JobOptions{IgnoreDecryptionErrors: true}, - ForegroundPriority, - ) - defer done() - - res, err := job.GetResult() + res, err := BuildRFC822(kr, msg, map[string][]byte{"attachID": att}, JobOptions{IgnoreDecryptionErrors: true}) require.NoError(t, err) section(t, res). @@ -1592,76 +1223,11 @@ func TestBuildCustomMessageOnlyAttachmentIsUndecryptable(t *testing.T) { expectTransferEncoding(isMissing()) } -func TestBuildFetchMessageFail(t *testing.T) { - m := gomock.NewController(t) - defer m.Finish() +func readFile(t *testing.T, path string) string { + t.Helper() - b := NewBuilder(2, 2) - defer b.Done() + b, err := os.ReadFile(filepath.Join("testdata", path)) + require.NoError(t, err) - kr := tests.MakeKeyRing(t) - msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) - - // Pretend the message cannot be fetched. - f := mocks.NewMockFetcher(m) - f.EXPECT().GetMessage(gomock.Any(), msg.ID).Return(nil, errors.New("oops")) - - // The job should fail, returning an error and a nil result. - job, done := b.NewJob(context.Background(), f, msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() - assert.Error(t, err) - assert.Nil(t, res) -} - -func TestBuildFetchAttachmentFail(t *testing.T) { - m := gomock.NewController(t) - defer m.Finish() - - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) - msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) - _ = addTestAttachment(t, kr, msg, "attachID", "file.png", "image/png", "attachment", "attachment") - - // Pretend the attachment cannot be fetched. - f := mocks.NewMockFetcher(m) - f.EXPECT().GetMessage(gomock.Any(), msg.ID).Return(msg, nil) - f.EXPECT().GetAttachment(gomock.Any(), msg.Attachments[0].ID).Return(nil, errors.New("oops")) - - // The job should fail, returning an error and a nil result. - job, done := b.NewJob(context.Background(), f, msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() - assert.Error(t, err) - assert.Nil(t, res) -} - -func TestBuildNoSuchKeyRing(t *testing.T) { - m := gomock.NewController(t) - defer m.Finish() - - b := NewBuilder(2, 2) - defer b.Done() - - kr := tests.MakeKeyRing(t) - msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now()) - - // Pretend there is no available keyring. - f := mocks.NewMockFetcher(m) - f.EXPECT().GetMessage(gomock.Any(), msg.ID).Return(msg, nil) - f.EXPECT().KeyRingForAddressID(msg.AddressID).Return(nil, errors.New("oops")) - - job, done := b.NewJob(context.Background(), f, msg.ID, ForegroundPriority) - defer done() - - res, err := job.GetResult() - assert.Error(t, err) - assert.Nil(t, res) - - // The returned error should be of this specific type. - assert.True(t, errors.Is(err, ErrNoSuchKeyRing)) + return string(b) } diff --git a/pkg/message/encrypt.go b/pkg/message/encrypt.go deleted file mode 100644 index b3c5927d..00000000 --- a/pkg/message/encrypt.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "bytes" - "encoding/base64" - "io" - "mime" - "mime/quotedprintable" - "strings" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime" - "github.com/emersion/go-message/textproto" - "github.com/pkg/errors" -) - -func EncryptRFC822(kr *crypto.KeyRing, r io.Reader) ([]byte, error) { - b, err := io.ReadAll(r) - if err != nil { - return nil, err - } - - header, body, err := readHeaderBody(b) - if err != nil { - return nil, err - } - - buf := new(bytes.Buffer) - - result, err := writeEncryptedPart(kr, header, bytes.NewReader(body)) - if err != nil { - return nil, err - } - - if err := textproto.WriteHeader(buf, *header); err != nil { - return nil, err - } - - if _, err := result.WriteTo(buf); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func writeEncryptedPart(kr *crypto.KeyRing, header *textproto.Header, r io.Reader) (io.WriterTo, error) { - decoder := getTransferDecoder(r, header.Get("Content-Transfer-Encoding")) - encoded := new(bytes.Buffer) - - contentType, contentParams, err := parseContentType(header.Get("Content-Type")) - // Ignoring invalid media parameter makes it work for invalid tutanota RFC2047-encoded attachment filenames since we often only really need the content type and not the optional media parameters. - if err != nil && !errors.Is(err, mime.ErrInvalidMediaParameter) { - return nil, err - } - - switch { - case contentType == "", strings.HasPrefix(contentType, "text/"), strings.HasPrefix(contentType, "message/"): - header.Del("Content-Transfer-Encoding") - - if charset, ok := contentParams["charset"]; ok { - if reader, err := pmmime.CharsetReader(charset, decoder); err == nil { - decoder = reader - - // We can decode the charset to utf-8 so let's set that as the content type charset parameter. - contentParams["charset"] = "utf-8" - - header.Set("Content-Type", mime.FormatMediaType(contentType, contentParams)) - } - } - - if err := encode(&writeCloser{encoded}, func(w io.Writer) error { - return writeEncryptedTextPart(w, decoder, kr) - }); err != nil { - return nil, err - } - - case contentType == "multipart/encrypted": - if _, err := encoded.ReadFrom(decoder); err != nil { - return nil, err - } - - case strings.HasPrefix(contentType, "multipart/"): - if err := encode(&writeCloser{encoded}, func(w io.Writer) error { - return writeEncryptedMultiPart(kr, w, header, decoder) - }); err != nil { - return nil, err - } - - default: - header.Set("Content-Transfer-Encoding", "base64") - - if err := encode(base64.NewEncoder(base64.StdEncoding, encoded), func(w io.Writer) error { - return writeEncryptedAttachmentPart(w, decoder, kr) - }); err != nil { - return nil, err - } - } - - return encoded, nil -} - -func writeEncryptedTextPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error { - dec, err := io.ReadAll(r) - if err != nil { - return err - } - - var arm string - - if msg, err := crypto.NewPGPMessageFromArmored(string(dec)); err != nil { - enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr) - if err != nil { - return err - } - - if arm, err = enc.GetArmored(); err != nil { - return err - } - } else if arm, err = msg.GetArmored(); err != nil { - return err - } - - if _, err := io.WriteString(w, arm); err != nil { - return err - } - - return nil -} - -func writeEncryptedAttachmentPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error { - dec, err := io.ReadAll(r) - if err != nil { - return err - } - - enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr) - if err != nil { - return err - } - - if _, err := w.Write(enc.GetBinary()); err != nil { - return err - } - - return nil -} - -func writeEncryptedMultiPart(kr *crypto.KeyRing, w io.Writer, header *textproto.Header, r io.Reader) error { - _, contentParams, err := parseContentType(header.Get("Content-Type")) - if err != nil { - return err - } - - scanner, err := newPartScanner(r, contentParams["boundary"]) - if err != nil { - return err - } - - parts, err := scanner.scanAll() - if err != nil { - return err - } - - writer := newPartWriter(w, contentParams["boundary"]) - - for _, part := range parts { - header, body, err := readHeaderBody(part.b) - if err != nil { - return err - } - - result, err := writeEncryptedPart(kr, header, bytes.NewReader(body)) - if err != nil { - return err - } - - if err := writer.createPart(func(w io.Writer) error { - if err := textproto.WriteHeader(w, *header); err != nil { - return err - } - - if _, err := result.WriteTo(w); err != nil { - return err - } - - return nil - }); err != nil { - return err - } - } - - return writer.done() -} - -func getTransferDecoder(r io.Reader, encoding string) io.Reader { - switch strings.ToLower(encoding) { - case "base64": - return base64.NewDecoder(base64.StdEncoding, r) - - case "quoted-printable": - return quotedprintable.NewReader(r) - - default: - return r - } -} - -func encode(wc io.WriteCloser, fn func(io.Writer) error) error { - if err := fn(wc); err != nil { - return err - } - - return wc.Close() -} - -type writeCloser struct { - io.Writer -} - -func (writeCloser) Close() error { return nil } - -func parseContentType(val string) (string, map[string]string, error) { - if val == "" { - val = "text/plain" - } - - return pmmime.ParseMediaType(val) -} diff --git a/pkg/message/encrypt_test.go b/pkg/message/encrypt_test.go deleted file mode 100644 index 11a0fcaf..00000000 --- a/pkg/message/encrypt_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "bytes" - "os" - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/require" -) - -func TestEncryptRFC822(t *testing.T) { - literal, err := os.ReadFile("testdata/text_plain_latin1.eml") - require.NoError(t, err) - - key, err := crypto.GenerateKey("name", "email", "rsa", 2048) - require.NoError(t, err) - - kr, err := crypto.NewKeyRing(key) - require.NoError(t, err) - - enc, err := EncryptRFC822(kr, bytes.NewReader(literal)) - require.NoError(t, err) - - section(t, enc). - expectContentType(is(`text/plain`)). - expectContentTypeParam(`charset`, is(`utf-8`)). - expectBody(decryptsTo(kr, `ééééééé`)) -} - -func TestEncryptRFC822Multipart(t *testing.T) { - literal, err := os.ReadFile("testdata/multipart_alternative_nested.eml") - require.NoError(t, err) - - key, err := crypto.GenerateKey("name", "email", "rsa", 2048) - require.NoError(t, err) - - kr, err := crypto.NewKeyRing(key) - require.NoError(t, err) - - enc, err := EncryptRFC822(kr, bytes.NewReader(literal)) - require.NoError(t, err) - - section(t, enc). - expectContentType(is(`multipart/alternative`)) - - section(t, enc, 1). - expectContentType(is(`multipart/alternative`)) - - section(t, enc, 1, 1). - expectContentType(is(`text/plain`)). - expectBody(decryptsTo(kr, "*multipart 1.1*\n\n")) - - section(t, enc, 1, 2). - expectContentType(is(`text/html`)). - expectBody(decryptsTo(kr, ` - - - - - multipart 1.2 - - -`)) - - section(t, enc, 2). - expectContentType(is(`multipart/alternative`)) - - section(t, enc, 2, 1). - expectContentType(is(`text/plain`)). - expectBody(decryptsTo(kr, "*multipart 2.1*\n\n")) - - section(t, enc, 2, 2). - expectContentType(is(`text/html`)). - expectBody(decryptsTo(kr, ` - - - - - multipart 2.2 - - -`)) -} diff --git a/pkg/message/envelope.go b/pkg/message/envelope.go deleted file mode 100644 index e7828e4f..00000000 --- a/pkg/message/envelope.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "net/mail" - "net/textproto" - "strings" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/emersion/go-imap" -) - -// GetEnvelope will prepare envelope from pmapi message and cached header. -func GetEnvelope(msg *pmapi.Message, header textproto.MIMEHeader) *imap.Envelope { - hdr := toMessageHeader(mail.Header(header)) - setMessageIDIfNeeded(msg, &hdr) - - return &imap.Envelope{ - Date: SanitizeMessageDate(msg.Time), - Subject: msg.Subject, - From: getAddresses([]*mail.Address{msg.Sender}), - Sender: getAddresses([]*mail.Address{msg.Sender}), - ReplyTo: getAddresses(msg.ReplyTos), - To: getAddresses(msg.ToList), - Cc: getAddresses(msg.CCList), - Bcc: getAddresses(msg.BCCList), - InReplyTo: hdr.Get("In-Reply-To"), - MessageId: hdr.Get("Message-Id"), - } -} - -func getAddresses(addrs []*mail.Address) (imapAddrs []*imap.Address) { - for _, a := range addrs { - if a == nil { - continue - } - - parts := strings.SplitN(a.Address, "@", 2) - if len(parts) != 2 { - continue - } - - imapAddrs = append(imapAddrs, &imap.Address{ - PersonalName: a.Name, - MailboxName: parts[0], - HostName: parts[1], - }) - } - - return -} diff --git a/pkg/message/flags.go b/pkg/message/flags.go deleted file mode 100644 index 06be0ae8..00000000 --- a/pkg/message/flags.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/emersion/go-imap" -) - -// Various client specific flags. -const ( - AppleMailJunkFlag = "$Junk" - ThunderbirdJunkFlag = "Junk" - ThunderbirdNonJunkFlag = "NonJunk" -) - -// GetFlags returns imap flags from pmapi message attributes. -func GetFlags(m *pmapi.Message) (flags []string) { - if !m.Unread { - flags = append(flags, imap.SeenFlag) - } - if !m.Has(pmapi.FlagSent) && !m.Has(pmapi.FlagReceived) { - flags = append(flags, imap.DraftFlag) - } - if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) { - flags = append(flags, imap.AnsweredFlag) - } - - hasSpam := false - - for _, l := range m.LabelIDs { - if l == pmapi.StarredLabel { - flags = append(flags, imap.FlaggedFlag) - } - if l == pmapi.SpamLabel { - flags = append(flags, AppleMailJunkFlag, ThunderbirdJunkFlag) - hasSpam = true - } - } - - if !hasSpam { - flags = append(flags, ThunderbirdNonJunkFlag) - } - - return -} diff --git a/pkg/message/init.go b/pkg/message/init.go deleted file mode 100644 index 3aa7d364..00000000 --- a/pkg/message/init.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "github.com/ProtonMail/go-rfc5322" - pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime" -) - -func init() { //nolint:gochecknoinits - rfc5322.CharsetReader = pmmime.CharsetReader -} diff --git a/pkg/message/message.go b/pkg/message/message.go index e0a8ccb4..961b3604 100644 --- a/pkg/message/message.go +++ b/pkg/message/message.go @@ -23,8 +23,4 @@ import ( "github.com/sirupsen/logrus" ) -const ( - rfc822Message = "message/rfc822" -) - var log = logrus.WithField("pkg", "pkg/message") //nolint:gochecknoglobals diff --git a/pkg/message/mocks/mocks.go b/pkg/message/mocks/mocks.go deleted file mode 100644 index 85629c14..00000000 --- a/pkg/message/mocks/mocks.go +++ /dev/null @@ -1,83 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ProtonMail/proton-bridge/v2/pkg/message (interfaces: Fetcher) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - context "context" - io "io" - reflect "reflect" - - crypto "github.com/ProtonMail/gopenpgp/v2/crypto" - pmapi "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - gomock "github.com/golang/mock/gomock" -) - -// MockFetcher is a mock of Fetcher interface. -type MockFetcher struct { - ctrl *gomock.Controller - recorder *MockFetcherMockRecorder -} - -// MockFetcherMockRecorder is the mock recorder for MockFetcher. -type MockFetcherMockRecorder struct { - mock *MockFetcher -} - -// NewMockFetcher creates a new mock instance. -func NewMockFetcher(ctrl *gomock.Controller) *MockFetcher { - mock := &MockFetcher{ctrl: ctrl} - mock.recorder = &MockFetcherMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockFetcher) EXPECT() *MockFetcherMockRecorder { - return m.recorder -} - -// GetAttachment mocks base method. -func (m *MockFetcher) GetAttachment(arg0 context.Context, arg1 string) (io.ReadCloser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAttachment", arg0, arg1) - ret0, _ := ret[0].(io.ReadCloser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAttachment indicates an expected call of GetAttachment. -func (mr *MockFetcherMockRecorder) GetAttachment(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockFetcher)(nil).GetAttachment), arg0, arg1) -} - -// GetMessage mocks base method. -func (m *MockFetcher) GetMessage(arg0 context.Context, arg1 string) (*pmapi.Message, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMessage", arg0, arg1) - ret0, _ := ret[0].(*pmapi.Message) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMessage indicates an expected call of GetMessage. -func (mr *MockFetcherMockRecorder) GetMessage(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockFetcher)(nil).GetMessage), arg0, arg1) -} - -// KeyRingForAddressID mocks base method. -func (m *MockFetcher) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0) - ret0, _ := ret[0].(*crypto.KeyRing) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// KeyRingForAddressID indicates an expected call of KeyRingForAddressID. -func (mr *MockFetcherMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockFetcher)(nil).KeyRingForAddressID), arg0) -} diff --git a/pkg/message/build_job.go b/pkg/message/options.go similarity index 100% rename from pkg/message/build_job.go rename to pkg/message/options.go diff --git a/pkg/message/parser.go b/pkg/message/parser.go index ad552aee..461587fa 100644 --- a/pkg/message/parser.go +++ b/pkg/message/parser.go @@ -23,98 +23,146 @@ import ( "io" "mime" "net/mail" - "net/textproto" "regexp" "strings" + "github.com/ProtonMail/gluon/rfc822" "github.com/ProtonMail/go-rfc5322" "github.com/ProtonMail/proton-bridge/v2/pkg/message/parser" pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime" - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" + "github.com/bradenaw/juniper/xslices" "github.com/emersion/go-message" "github.com/jaytaylor/html2text" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "gitlab.protontech.ch/go/liteapi" ) -// Parse parses RAW message. -func Parse(r io.Reader) (m *pmapi.Message, mimeBody, plainBody string, attReaders []io.Reader, err error) { - defer func() { - r := recover() - if r == nil { - return - } +type MIMEBody string - err = fmt.Errorf("panic while parsing message: %v", r) +type Body string + +type Message struct { + Header mail.Header + MIMEBody MIMEBody + RichBody Body + PlainBody Body + Time int64 + ExternalID string + + Subject string + Sender *mail.Address + ToList []*mail.Address + CCList []*mail.Address + BCCList []*mail.Address + ReplyTos []*mail.Address + + MIMEType rfc822.MIMEType + Attachments []Attachment +} + +func (m *Message) Recipients() []string { + var recipients []string + + for _, addresses := range [][]*mail.Address{m.ToList, m.CCList, m.BCCList} { + recipients = append(recipients, xslices.Map(addresses, func(address *mail.Address) string { + return address.Address + })...) + } + + return recipients +} + +type Attachment struct { + Header mail.Header + Name string + ContentID string + MIMEType string + Disposition string + Data []byte +} + +// Parse parses an RFC822 message. +func Parse(r io.Reader) (m Message, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic while parsing message: %v", r) + } }() p, err := parser.New(r) if err != nil { - return nil, "", "", nil, errors.Wrap(err, "failed to create new parser") + return Message{}, errors.Wrap(err, "failed to create new parser") } - m, plainBody, attReaders, err = ParserWithParser(p) - if err != nil { - return nil, "", "", nil, errors.Wrap(err, "failed to parse the message") - } - - mimeBody, err = BuildMIMEBody(p) - if err != nil { - return nil, "", "", nil, errors.Wrap(err, "failed to build mime body") - } - - return m, mimeBody, plainBody, attReaders, nil + return parse(p) } -// ParserWithParser parses message from Parser without building MIME body. -func ParserWithParser(p *parser.Parser) (m *pmapi.Message, plainBody string, attReaders []io.Reader, err error) { - logrus.Trace("Parsing message") +// Parse parses an RFC822 message using an existing parser. +func ParseWithParser(p *parser.Parser) (m Message, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic while parsing message: %v", r) + } + }() - if err = convertEncodedTransferEncoding(p); err != nil { - err = errors.Wrap(err, "failed to convert encoded transfer encodings") - return - } - - if err = convertForeignEncodings(p); err != nil { - err = errors.Wrap(err, "failed to convert foreign encodings") - return - } - - m = pmapi.NewMessage() - - if err = parseMessageHeader(m, p.Root().Header); err != nil { - err = errors.Wrap(err, "failed to parse message header") - return - } - - if m.Attachments, attReaders, err = collectAttachments(p); err != nil { - err = errors.Wrap(err, "failed to collect attachments") - return - } - - if m.Body, plainBody, err = buildBodies(p); err != nil { - err = errors.Wrap(err, "failed to build bodies") - return - } - - if m.MIMEType, err = determineMIMEType(p); err != nil { - err = errors.Wrap(err, "failed to determine mime type") - return - } - - return m, plainBody, attReaders, nil + return parse(p) } -// BuildMIMEBody builds mime body from the parser returned by NewParser. -func BuildMIMEBody(p *parser.Parser) (mimeBody string, err error) { - mimeBodyBuffer := new(bytes.Buffer) - - if err = p.NewWriter().Write(mimeBodyBuffer); err != nil { - err = errors.Wrap(err, "failed to write out mime message") - return +func parse(p *parser.Parser) (Message, error) { + if err := convertEncodedTransferEncoding(p); err != nil { + return Message{}, errors.Wrap(err, "failed to convert encoded transfer encoding") } - return mimeBodyBuffer.String(), nil + if err := convertForeignEncodings(p); err != nil { + return Message{}, errors.Wrap(err, "failed to convert foreign encodings") + } + + m, err := parseMessageHeader(p.Root().Header) + if err != nil { + return Message{}, errors.Wrap(err, "failed to parse message header") + } + + atts, err := collectAttachments(p) + if err != nil { + return Message{}, errors.Wrap(err, "failed to collect attachments") + } + + m.Attachments = atts + + richBody, plainBody, err := buildBodies(p) + if err != nil { + return Message{}, errors.Wrap(err, "failed to build bodies") + } + + mimeBody, err := buildMIMEBody(p) + if err != nil { + return Message{}, errors.Wrap(err, "failed to build mime body") + } + + m.RichBody = Body(richBody) + m.PlainBody = Body(plainBody) + m.MIMEBody = MIMEBody(mimeBody) + + mimeType, err := determineMIMEType(p) + if err != nil { + return Message{}, errors.Wrap(err, "failed to get mime type") + } + + m.MIMEType = rfc822.MIMEType(mimeType) + + return m, nil +} + +// buildMIMEBody builds mime body from the parser returned by NewParser. +func buildMIMEBody(p *parser.Parser) (mimeBody string, err error) { + buf := new(bytes.Buffer) + + if err := p.NewWriter().Write(buf); err != nil { + return "", fmt.Errorf("failed to write message: %w", err) + } + + return buf.String(), nil } // convertEncodedTransferEncoding decodes any RFC2047-encoded content transfer encodings. @@ -158,33 +206,30 @@ func convertForeignEncodings(p *parser.Parser) error { Walk() } -func collectAttachments(p *parser.Parser) ([]*pmapi.Attachment, []io.Reader, error) { +func collectAttachments(p *parser.Parser) ([]Attachment, error) { var ( - atts []*pmapi.Attachment - data []io.Reader + atts []Attachment err error ) w := p.NewWalker(). RegisterContentDispositionHandler("attachment", func(p *parser.Part) error { - att, err := parseAttachment(p.Header) + att, err := parseAttachment(p.Header, p.Body) if err != nil { return err } atts = append(atts, att) - data = append(data, bytes.NewReader(p.Body)) return nil }). RegisterContentTypeHandler("text/calendar", func(p *parser.Part) error { - att, err := parseAttachment(p.Header) + att, err := parseAttachment(p.Header, p.Body) if err != nil { return err } atts = append(atts, att) - data = append(data, bytes.NewReader(p.Body)) return nil }). @@ -196,22 +241,21 @@ func collectAttachments(p *parser.Parser) ([]*pmapi.Attachment, []io.Reader, err return nil } - att, err := parseAttachment(p.Header) + att, err := parseAttachment(p.Header, p.Body) if err != nil { return err } atts = append(atts, att) - data = append(data, bytes.NewReader(p.Body)) return nil }) if err = w.Walk(); err != nil { - return nil, nil, err + return nil, err } - return atts, data, nil + return atts, nil } // buildBodies collects all text/html and text/plain parts and returns two bodies, @@ -400,24 +444,14 @@ func getPlainBody(part *parser.Part) []byte { } } -func AttachPublicKey(p *parser.Parser, key, keyName string) { - h := message.Header{} +func parseMessageHeader(h message.Header) (Message, error) { //nolint:funlen + var m Message - h.Set("Content-Type", fmt.Sprintf(`application/pgp-keys; name="%v.asc"; filename="%v.asc"`, keyName, keyName)) - h.Set("Content-Disposition", fmt.Sprintf(`attachment; name="%v.asc"; filename="%v.asc"`, keyName, keyName)) - h.Set("Content-Transfer-Encoding", "base64") - - p.Root().AddChild(&parser.Part{ - Header: h, - Body: []byte(key), - }) -} - -func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:funlen mimeHeader, err := toMailHeader(h) if err != nil { - return err + return Message{}, err } + m.Header = mimeHeader fields := h.Fields() @@ -428,7 +462,7 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:fun s, err := fields.Text() if err != nil { if s, err = pmmime.DecodeHeader(fields.Value()); err != nil { - return errors.Wrap(err, "failed to parse subject") + return Message{}, errors.Wrap(err, "failed to parse subject") } } @@ -437,7 +471,7 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:fun case "from": sender, err := rfc5322.ParseAddressList(fields.Value()) if err != nil { - return errors.Wrap(err, "failed to parse from") + return Message{}, errors.Wrap(err, "failed to parse from") } if len(sender) > 0 { m.Sender = sender[0] @@ -446,35 +480,35 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:fun case "to": toList, err := rfc5322.ParseAddressList(fields.Value()) if err != nil { - return errors.Wrap(err, "failed to parse to") + return Message{}, errors.Wrap(err, "failed to parse to") } m.ToList = toList case "reply-to": replyTos, err := rfc5322.ParseAddressList(fields.Value()) if err != nil { - return errors.Wrap(err, "failed to parse reply-to") + return Message{}, errors.Wrap(err, "failed to parse reply-to") } m.ReplyTos = replyTos case "cc": ccList, err := rfc5322.ParseAddressList(fields.Value()) if err != nil { - return errors.Wrap(err, "failed to parse cc") + return Message{}, errors.Wrap(err, "failed to parse cc") } m.CCList = ccList case "bcc": bccList, err := rfc5322.ParseAddressList(fields.Value()) if err != nil { - return errors.Wrap(err, "failed to parse bcc") + return Message{}, errors.Wrap(err, "failed to parse bcc") } m.BCCList = bccList case "date": date, err := rfc5322.ParseDateTime(fields.Value()) if err != nil { - return errors.Wrap(err, "failed to parse date") + return Message{}, errors.Wrap(err, "failed to parse date") } m.Time = date.Unix() @@ -483,48 +517,47 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:fun } } - return nil + return m, nil } -func parseAttachment(h message.Header) (*pmapi.Attachment, error) { - att := &pmapi.Attachment{} +func parseAttachment(h message.Header, body []byte) (Attachment, error) { + att := Attachment{ + Data: body, + } - mimeHeader, err := toMIMEHeader(h) + mimeHeader, err := toMailHeader(h) if err != nil { - return nil, err + return Attachment{}, err } att.Header = mimeHeader mimeType, mimeTypeParams, err := h.ContentType() if err != nil { - return nil, err + return Attachment{}, err } att.MIMEType = mimeType // Prefer attachment name from filename param in content disposition. // If not available, try to get it from name param in content type. // Otherwise fallback to attachment.bin. - _, dispParams, dispErr := h.ContentDisposition() - if dispErr != nil { - ext, err := mime.ExtensionsByType(att.MIMEType) - if err != nil { - return nil, err - } + if disp, dispParams, err := h.ContentDisposition(); err == nil { + att.Disposition = disp - if len(ext) > 0 { - att.Name = "attachment" + ext[0] + if filename, ok := dispParams["filename"]; ok { + att.Name = filename } - } else { - att.Name = dispParams["filename"] } + if att.Name == "" { - att.Name = mimeTypeParams["name"] - } - if att.Name == "" && mimeType == rfc822Message { - att.Name = "message.eml" - } - if att.Name == "" { - att.Name = "attachment.bin" + if filename, ok := mimeTypeParams["name"]; ok { + att.Name = filename + } else if mimeType == string(rfc822.MessageRFC822) { + att.Name = "message.eml" + } else if ext, err := mime.ExtensionsByType(att.MIMEType); err == nil && len(ext) > 0 { + att.Name = "attachment" + ext[0] + } else { + att.Name = "attachment.bin" + } } // Only set ContentID if it should be inline; @@ -534,9 +567,12 @@ func parseAttachment(h message.Header) (*pmapi.Attachment, error) { // (This is necessary because some clients don't set Content-Disposition at all, // so we need to rely on other information to deduce if it's inline or attachment.) if h.Has("Content-Disposition") { - if disp, _, err := h.ContentDisposition(); err != nil { - return nil, err - } else if disp == pmapi.DispositionInline { + disp, _, err := h.ContentDisposition() + if err != nil { + return Attachment{}, err + } + + if disp == string(liteapi.InlineDisposition) { att.ContentID = strings.Trim(h.Get("Content-Id"), " <>") } } else if h.Has("Content-Id") { @@ -559,19 +595,6 @@ func toMailHeader(h message.Header) (mail.Header, error) { return mimeHeader, nil } -func toMIMEHeader(h message.Header) (textproto.MIMEHeader, error) { - mimeHeader := make(textproto.MIMEHeader) - - if err := forEachDecodedHeaderField(h, func(key, val string) error { - mimeHeader[key] = []string{val} - return nil - }); err != nil { - return nil, err - } - - return mimeHeader, nil -} - func forEachDecodedHeaderField(h message.Header, fn func(string, string) error) error { fields := h.Fields() diff --git a/pkg/message/parser/parser.go b/pkg/message/parser/parser.go index 0fbd5c35..06095bc8 100644 --- a/pkg/message/parser/parser.go +++ b/pkg/message/parser/parser.go @@ -18,6 +18,7 @@ package parser import ( + "fmt" "io" "github.com/emersion/go-message" @@ -67,6 +68,19 @@ func (p *Parser) Root() *Part { return p.root } +func (p *Parser) AttachPublicKey(key, keyName string) { + h := message.Header{} + + h.Set("Content-Type", fmt.Sprintf(`application/pgp-keys; name="%v.asc"; filename="%v.asc"`, keyName, keyName)) + h.Set("Content-Disposition", fmt.Sprintf(`attachment; name="%v.asc"; filename="%v.asc"`, keyName, keyName)) + h.Set("Content-Transfer-Encoding", "base64") + + p.Root().AddChild(&Part{ + Header: h, + Body: []byte(key), + }) +} + // Section returns the message part referred to by the given section. A section // is zero or more integers. For example, section 1.2.3 will return the third // part of the second part of the first part of the message. diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go index 714e2c6c..d684d7a9 100644 --- a/pkg/message/parser_test.go +++ b/pkg/message/parser_test.go @@ -18,6 +18,7 @@ package message import ( + "bytes" "image/png" "io" "os" @@ -33,129 +34,129 @@ import ( func TestParseLongHeaderLine(t *testing.T) { f := getFileReader("long_header_line.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseLongHeaderLineMultiline(t *testing.T) { f := getFileReader("long_header_line_multiline.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlain(t *testing.T) { f := getFileReader("text_plain.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainUTF8(t *testing.T) { f := getFileReader("text_plain_utf8.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainLatin1(t *testing.T) { f := getFileReader("text_plain_latin1.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "ééééééé", m.Body) - assert.Equal(t, "ééééééé", plainBody) + assert.Equal(t, "ééééééé", string(m.RichBody)) + assert.Equal(t, "ééééééé", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainUTF8Subject(t *testing.T) { f := getFileReader("text_plain_utf8_subject.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) assert.Equal(t, `汉字汉字汉`, m.Subject) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainLatin2Subject(t *testing.T) { f := getFileReader("text_plain_latin2_subject.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) assert.Equal(t, `If you can read this you understand the example.`, m.Subject) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainUnknownCharsetIsActuallyLatin1(t *testing.T) { f := getFileReader("text_plain_unknown_latin1.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "ééééééé", m.Body) - assert.Equal(t, "ééééééé", plainBody) + assert.Equal(t, "ééééééé", string(m.RichBody)) + assert.Equal(t, "ééééééé", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainUnknownCharsetIsActuallyLatin2(t *testing.T) { f := getFileReader("text_plain_unknown_latin2.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) @@ -167,97 +168,97 @@ func TestParseTextPlainUnknownCharsetIsActuallyLatin2(t *testing.T) { expect, _ := charmap.ISO8859_1.NewDecoder().Bytes(latin2) assert.NotEqual(t, []byte("řšřšřš"), expect) - assert.Equal(t, string(expect), m.Body) - assert.Equal(t, string(expect), plainBody) + assert.Equal(t, string(expect), string(m.RichBody)) + assert.Equal(t, string(expect), string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainAlready7Bit(t *testing.T) { f := getFileReader("text_plain_7bit.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainWithOctetAttachment(t *testing.T) { f := getFileReader("text_plain_octet_attachment.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - require.Len(t, attReaders, 1) - assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!") + require.Len(t, m.Attachments, 1) + assert.Equal(t, string(m.Attachments[0].Data), "if you are reading this, hi!") } func TestParseTextPlainWithOctetAttachmentGoodFilename(t *testing.T) { f := getFileReader("text_plain_octet_attachment_good_2231_filename.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 1) - assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!") + assert.Len(t, m.Attachments, 1) + assert.Equal(t, string(m.Attachments[0].Data), "if you are reading this, hi!") assert.Equal(t, "😁😂.txt", m.Attachments[0].Name) } func TestParseTextPlainWithRFC822Attachment(t *testing.T) { f := getFileReader("text_plain_rfc822_attachment.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 1) + assert.Len(t, m.Attachments, 1) assert.Equal(t, "message.eml", m.Attachments[0].Name) } func TestParseTextPlainWithOctetAttachmentBadFilename(t *testing.T) { f := getFileReader("text_plain_octet_attachment_bad_2231_filename.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 1) - assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!") + assert.Len(t, m.Attachments, 1) + assert.Equal(t, string(m.Attachments[0].Data), "if you are reading this, hi!") assert.Equal(t, "attachment.bin", m.Attachments[0].Name) } func TestParseTextPlainWithOctetAttachmentNameInContentType(t *testing.T) { f := getFileReader("text_plain_octet_attachment_name_in_contenttype.eml") - m, _, _, _, err := Parse(f) //nolint:dogsled + m, err := Parse(f) //nolint:dogsled require.NoError(t, err) assert.Equal(t, "attachment-contenttype.txt", m.Attachments[0].Name) @@ -266,7 +267,7 @@ func TestParseTextPlainWithOctetAttachmentNameInContentType(t *testing.T) { func TestParseTextPlainWithOctetAttachmentNameConflict(t *testing.T) { f := getFileReader("text_plain_octet_attachment_name_conflict.eml") - m, _, _, _, err := Parse(f) //nolint:dogsled + m, err := Parse(f) //nolint:dogsled require.NoError(t, err) assert.Equal(t, "attachment-disposition.txt", m.Attachments[0].Name) @@ -275,49 +276,49 @@ func TestParseTextPlainWithOctetAttachmentNameConflict(t *testing.T) { func TestParseTextPlainWithPlainAttachment(t *testing.T) { f := getFileReader("text_plain_plain_attachment.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - require.Len(t, attReaders, 1) - assert.Equal(t, readerToString(attReaders[0]), "attachment") + require.Len(t, m.Attachments, 1) + assert.Equal(t, string(m.Attachments[0].Data), "attachment") } func TestParseTextPlainEmptyAddresses(t *testing.T) { f := getFileReader("text_plain_empty_addresses.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextPlainWithImageInline(t *testing.T) { f := getFileReader("text_plain_image_inline.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) // The inline image is an 8x8 mic-dropping gopher. - require.Len(t, attReaders, 1) - img, err := png.DecodeConfig(attReaders[0]) + require.Len(t, m.Attachments, 1) + img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data)) require.NoError(t, err) assert.Equal(t, 8, img.Width) assert.Equal(t, 8, img.Height) @@ -326,111 +327,111 @@ func TestParseTextPlainWithImageInline(t *testing.T) { func TestParseTextPlainWithDuplicateCharset(t *testing.T) { f := getFileReader("text_plain_duplicate_charset.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseWithMultipleTextParts(t *testing.T) { f := getFileReader("multiple_text_parts.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body\nsome other part of the message", m.Body) - assert.Equal(t, "body\nsome other part of the message", plainBody) + assert.Equal(t, "body\nsome other part of the message", string(m.RichBody)) + assert.Equal(t, "body\nsome other part of the message", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextHTML(t *testing.T) { f := getFileReader("text_html.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "This is body of HTML mail without attachment", m.Body) - assert.Equal(t, "This is body of *HTML mail* without attachment", plainBody) + assert.Equal(t, "This is body of HTML mail without attachment", string(m.RichBody)) + assert.Equal(t, "This is body of *HTML mail* without attachment", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextHTMLAlready7Bit(t *testing.T) { f := getFileReader("text_html_7bit.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) assert.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "This is body of HTML mail without attachment", m.Body) - assert.Equal(t, "This is body of *HTML mail* without attachment", plainBody) + assert.Equal(t, "This is body of HTML mail without attachment", string(m.RichBody)) + assert.Equal(t, "This is body of *HTML mail* without attachment", string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseTextHTMLWithOctetAttachment(t *testing.T) { f := getFileReader("text_html_octet_attachment.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "This is body of HTML mail with attachment", m.Body) - assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody) + assert.Equal(t, "This is body of HTML mail with attachment", string(m.RichBody)) + assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody)) - require.Len(t, attReaders, 1) - assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!") + require.Len(t, m.Attachments, 1) + assert.Equal(t, string(m.Attachments[0].Data), "if you are reading this, hi!") } func TestParseTextHTMLWithPlainAttachment(t *testing.T) { f := getFileReader("text_html_plain_attachment.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) // BAD: plainBody should not be empty! - assert.Equal(t, "This is body of HTML mail with attachment", m.Body) - assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody) + assert.Equal(t, "This is body of HTML mail with attachment", string(m.RichBody)) + assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody)) - require.Len(t, attReaders, 1) - assert.Equal(t, readerToString(attReaders[0]), "attachment") + require.Len(t, m.Attachments, 1) + assert.Equal(t, string(m.Attachments[0].Data), "attachment") } func TestParseTextHTMLWithImageInline(t *testing.T) { f := getFileReader("text_html_image_inline.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) assert.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "This is body of HTML mail with attachment", m.Body) - assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody) + assert.Equal(t, "This is body of HTML mail with attachment", string(m.RichBody)) + assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody)) // The inline image is an 8x8 mic-dropping gopher. - require.Len(t, attReaders, 1) - img, err := png.DecodeConfig(attReaders[0]) + require.Len(t, m.Attachments, 1) + img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data)) require.NoError(t, err) assert.Equal(t, 8, img.Width) assert.Equal(t, 8, img.Height) @@ -441,40 +442,42 @@ func TestParseWithAttachedPublicKey(t *testing.T) { p, err := parser.New(f) require.NoError(t, err) - m, plainBody, attReaders, err := ParserWithParser(p) - AttachPublicKey(p, "publickey", "publickeyname") + + m, err := ParseWithParser(p) require.NoError(t, err) + p.AttachPublicKey("publickey", "publickeyname") + assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "body", m.Body) - assert.Equal(t, "body", plainBody) + assert.Equal(t, "body", string(m.RichBody)) + assert.Equal(t, "body", string(m.PlainBody)) // The pubkey should not be collected as an attachment. // We upload the pubkey when creating the draft. - require.Len(t, attReaders, 0) + require.Len(t, m.Attachments, 0) } func TestParseTextHTMLWithEmbeddedForeignEncoding(t *testing.T) { f := getFileReader("text_html_embedded_foreign_encoding.eml") - m, _, plainBody, attReaders, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, `latin2 řšřš`, m.Body) - assert.Equal(t, `latin2 řšřš`, plainBody) + assert.Equal(t, `latin2 řšřš`, string(m.RichBody)) + assert.Equal(t, `latin2 řšřš`, string(m.PlainBody)) - assert.Len(t, attReaders, 0) + assert.Len(t, m.Attachments, 0) } func TestParseMultipartAlternative(t *testing.T) { f := getFileReader("multipart_alternative.eml") - m, _, plainBody, _, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"schizofrenic" `, m.Sender.String()) @@ -487,15 +490,15 @@ func TestParseMultipartAlternative(t *testing.T) { aoeuaoeu -`, m.Body) +`, string(m.RichBody)) - assert.Equal(t, "*aoeuaoeu*\n\n", plainBody) + assert.Equal(t, "*aoeuaoeu*\n\n", string(m.PlainBody)) } func TestParseMultipartAlternativeNested(t *testing.T) { f := getFileReader("multipart_alternative_nested.eml") - m, _, plainBody, _, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"schizofrenic" `, m.Sender.String()) @@ -508,15 +511,15 @@ func TestParseMultipartAlternativeNested(t *testing.T) { multipart 2.2 -`, m.Body) +`, string(m.RichBody)) - assert.Equal(t, "*multipart 2.1*\n\n", plainBody) + assert.Equal(t, "*multipart 2.1*\n\n", string(m.PlainBody)) } func TestParseMultipartAlternativeLatin1(t *testing.T) { f := getFileReader("multipart_alternative_latin1.eml") - m, _, plainBody, _, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"schizofrenic" `, m.Sender.String()) @@ -529,52 +532,52 @@ func TestParseMultipartAlternativeLatin1(t *testing.T) { aoeuaoeu -`, m.Body) +`, string(m.RichBody)) - assert.Equal(t, "*aoeuaoeu*\n\n", plainBody) + assert.Equal(t, "*aoeuaoeu*\n\n", string(m.PlainBody)) } func TestParseWithTrailingEndOfMailIndicator(t *testing.T) { f := getFileReader("text_html_trailing_end_of_mail.eml") - m, _, plainBody, _, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) - assert.Equal(t, "boo!", m.Body) - assert.Equal(t, "boo!", plainBody) + assert.Equal(t, "boo!", string(m.RichBody)) + assert.Equal(t, "boo!", string(m.PlainBody)) } func TestParseEncodedContentType(t *testing.T) { f := getFileReader("rfc2047-content-transfer-encoding.eml") - m, _, plainBody, _, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, ``, m.ToList[0].String()) - assert.Equal(t, "bodybodybody\n", plainBody) + assert.Equal(t, "bodybodybody\n", string(m.PlainBody)) } func TestParseNonEncodedContentType(t *testing.T) { f := getFileReader("non-encoded-content-transfer-encoding.eml") - m, _, plainBody, _, err := Parse(f) + m, err := Parse(f) require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, ``, m.ToList[0].String()) - assert.Equal(t, "bodybodybody\n", plainBody) + assert.Equal(t, "bodybodybody\n", string(m.PlainBody)) } func TestParseEncodedContentTypeBad(t *testing.T) { f := getFileReader("rfc2047-content-transfer-encoding-bad.eml") - _, _, _, _, err := Parse(f) //nolint:dogsled + _, err := Parse(f) //nolint:dogsled require.Error(t, err) } @@ -587,7 +590,7 @@ func (panicReader) Read(p []byte) (int, error) { func TestParsePanic(t *testing.T) { var err error require.NotPanics(t, func() { - _, _, _, _, err = Parse(&panicReader{}) + _, err = Parse(&panicReader{}) }) require.Error(t, err) } @@ -600,12 +603,3 @@ func getFileReader(filename string) io.Reader { return f } - -func readerToString(r io.Reader) string { - b, err := io.ReadAll(r) - if err != nil { - panic(err) - } - - return string(b) -} diff --git a/pkg/message/scanner.go b/pkg/message/scanner.go deleted file mode 100644 index 99711359..00000000 --- a/pkg/message/scanner.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "bufio" - "bytes" - "errors" - "io" -) - -type partScanner struct { - r *bufio.Reader - - boundary string - progress int -} - -type part struct { - b []byte - offset int -} - -func newPartScanner(r io.Reader, boundary string) (*partScanner, error) { - scanner := &partScanner{r: bufio.NewReader(r), boundary: boundary} - - if _, _, err := scanner.readToBoundary(); err != nil { - return nil, err - } - - return scanner, nil -} - -func (s *partScanner) scanAll() ([]part, error) { - var parts []part - - for { - offset := s.progress - - b, more, err := s.readToBoundary() - if err != nil { - return nil, err - } - - if !more { - return parts, nil - } - - parts = append(parts, part{b: b, offset: offset}) - } -} - -func (s *partScanner) readToBoundary() ([]byte, bool, error) { - var res []byte - - for { - line, err := s.r.ReadBytes('\n') - if err != nil { - if !errors.Is(err, io.EOF) { - return nil, false, err - } - - if len(line) == 0 { - return nil, false, nil - } - } - - s.progress += len(line) - - switch { - case bytes.HasPrefix(bytes.TrimSpace(line), []byte("--"+s.boundary)): - return bytes.TrimSuffix(bytes.TrimSuffix(res, []byte("\n")), []byte("\r")), true, nil - - case bytes.HasSuffix(bytes.TrimSpace(line), []byte(s.boundary+"--")): - return bytes.TrimSuffix(bytes.TrimSuffix(res, []byte("\n")), []byte("\r")), false, nil - - default: - res = append(res, line...) - } - } -} diff --git a/pkg/message/scanner_test.go b/pkg/message/scanner_test.go deleted file mode 100644 index 884eaa24..00000000 --- a/pkg/message/scanner_test.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestScanner(t *testing.T) { - const literal = `this part of the text should be ignored - ---longrandomstring - -body1 - ---longrandomstring - -body2 - ---longrandomstring-- -` - - scanner, err := newPartScanner(strings.NewReader(literal), "longrandomstring") - require.NoError(t, err) - - parts, err := scanner.scanAll() - require.NoError(t, err) - - assert.Equal(t, "\nbody1\n", string(parts[0].b)) - assert.Equal(t, "\nbody2\n", string(parts[1].b)) - - assert.Equal(t, "\nbody1\n", literal[parts[0].offset:parts[0].offset+len(parts[0].b)]) - assert.Equal(t, "\nbody2\n", literal[parts[1].offset:parts[1].offset+len(parts[1].b)]) -} - -func TestScannerNested(t *testing.T) { - const literal = `This is the preamble. It is to be ignored, though it -is a handy place for mail composers to include an -explanatory note to non-MIME compliant readers. ---simple boundary -Content-type: multipart/mixed; boundary="nested boundary" - -This is the preamble. It is to be ignored, though it -is a handy place for mail composers to include an -explanatory note to non-MIME compliant readers. ---nested boundary -Content-type: text/plain; charset=us-ascii - -This part does not end with a linebreak. ---nested boundary -Content-type: text/plain; charset=us-ascii - -This part does end with a linebreak. - ---nested boundary-- ---simple boundary -Content-type: text/plain; charset=us-ascii - -This part does end with a linebreak. - ---simple boundary-- -This is the epilogue. It is also to be ignored. -` - - scanner, err := newPartScanner(strings.NewReader(literal), "simple boundary") - require.NoError(t, err) - - parts, err := scanner.scanAll() - require.NoError(t, err) - - assert.Equal(t, `Content-type: multipart/mixed; boundary="nested boundary" - -This is the preamble. It is to be ignored, though it -is a handy place for mail composers to include an -explanatory note to non-MIME compliant readers. ---nested boundary -Content-type: text/plain; charset=us-ascii - -This part does not end with a linebreak. ---nested boundary -Content-type: text/plain; charset=us-ascii - -This part does end with a linebreak. - ---nested boundary--`, string(parts[0].b)) - assert.Equal(t, `Content-type: text/plain; charset=us-ascii - -This part does end with a linebreak. -`, string(parts[1].b)) -} - -func TestScannerNoFinalLinebreak(t *testing.T) { - const literal = `--nested boundary -Content-type: text/plain; charset=us-ascii - -This part does not end with a linebreak. ---nested boundary -Content-type: text/plain; charset=us-ascii - -This part does end with a linebreak. - ---nested boundary--` - - scanner, err := newPartScanner(strings.NewReader(literal), "nested boundary") - require.NoError(t, err) - - parts, err := scanner.scanAll() - require.NoError(t, err) - - assert.Equal(t, `Content-type: text/plain; charset=us-ascii - -This part does not end with a linebreak.`, string(parts[0].b)) - assert.Equal(t, `Content-type: text/plain; charset=us-ascii - -This part does end with a linebreak. -`, string(parts[1].b)) -} diff --git a/pkg/message/section.go b/pkg/message/section.go deleted file mode 100644 index 7be0acc7..00000000 --- a/pkg/message/section.go +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "bufio" - "bytes" - "io" - "net/textproto" - "strconv" - "strings" - - pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime" - "github.com/emersion/go-imap" - "github.com/pkg/errors" - "github.com/vmihailenco/msgpack/v5" -) - -// BodyStructure is used to parse an email into MIME sections and then generate -// body structure for IMAP server. -type BodyStructure map[string]*SectionInfo - -// SectionInfo is used to hold data about parts of each section. -type SectionInfo struct { - Header []byte - Start, BSize, Size, Lines int - reader io.Reader - isHeaderReadFinished bool -} - -// Read will also count the final size of section. -func (si *SectionInfo) Read(p []byte) (n int, err error) { - n, err = si.reader.Read(p) - si.Size += n - si.Lines += bytes.Count(p, []byte("\n")) - - si.readHeader(p) - return -} - -// readHeader appends read data to Header until empty line is found. -func (si *SectionInfo) readHeader(p []byte) { - if si.isHeaderReadFinished { - return - } - - si.Header = append(si.Header, p...) - - if i := bytes.Index(si.Header, []byte("\n\r\n")); i > 0 { - si.Header = si.Header[:i+3] - si.isHeaderReadFinished = true - return - } - - // textproto works also with simple line ending so we should be liberal - // as well. - if i := bytes.Index(si.Header, []byte("\n\n")); i > 0 { - si.Header = si.Header[:i+2] - si.isHeaderReadFinished = true - } -} - -// GetMIMEHeader parses bytes and return MIME header. -func (si *SectionInfo) GetMIMEHeader() (textproto.MIMEHeader, error) { - return textproto.NewReader(bufio.NewReader(bytes.NewReader(si.Header))).ReadMIMEHeader() -} - -func NewBodyStructure(reader io.Reader) (structure *BodyStructure, err error) { - structure = &BodyStructure{} - err = structure.Parse(reader) - return -} - -// DeserializeBodyStructure will create new structure from msgpack bytes. -func DeserializeBodyStructure(raw []byte) (*BodyStructure, error) { - bs := &BodyStructure{} - err := msgpack.Unmarshal(raw, bs) - if err != nil { - return nil, errors.Wrap(err, "cannot deserialize bodystructure") - } - return bs, err -} - -// Serialize will write msgpack bytes. -func (bs *BodyStructure) Serialize() ([]byte, error) { - data, err := msgpack.Marshal(bs) - if err != nil { - return nil, errors.Wrap(err, "cannot serialize bodystructure") - } - return data, nil -} - -// Parse will read the mail and create all body structures. -func (bs *BodyStructure) Parse(r io.Reader) error { - return bs.parseAllChildSections(r, []int{}, 0) -} - -func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, start int) (err error) { //nolint:funlen - info := &SectionInfo{ - Start: start, - Size: 0, - BSize: 0, - Lines: 0, - reader: r, - } - - bufInfo := bufio.NewReader(info) - tp := textproto.NewReader(bufInfo) - - tpHeader, err := tp.ReadMIMEHeader() - if err != nil { - return - } - - bodyInfo := &SectionInfo{reader: tp.R} - bodyReader := bufio.NewReader(bodyInfo) - - mediaType, params, _ := pmmime.ParseMediaType(tpHeader.Get("Content-Type")) - - // If multipart, call getAllParts, else read to count lines. - if (strings.HasPrefix(mediaType, "multipart/") || mediaType == rfc822Message) && params["boundary"] != "" { - nextPath := getChildPath(currentPath) - - var br *boundaryReader - br, err = newBoundaryReader(bodyReader, params["boundary"]) - // New reader seeks first boundary. - if err != nil { - // Return also EOF. - return - } - - for err == nil { - start += br.skipped - part := &bytes.Buffer{} - err = br.writeNextPartTo(part) - if err != nil { - break - } - err = bs.parseAllChildSections(part, nextPath, start) - part.Reset() - nextPath[len(nextPath)-1]++ - } - br.reader = nil - - if err == io.EOF { - err = nil - } - if err != nil { - return - } - } else { - // Count length. - _, _ = bodyReader.WriteTo(io.Discard) - } - - // Clear all buffers. - bodyReader = nil //nolint:wastedassign // just to be sure we clear garbage collector - bodyInfo.reader = nil - tp.R = nil - tp = nil //nolint:wastedassign // just to be sure we clear garbage collector - bufInfo = nil //nolint:ineffassign,wastedassign // just to be sure we clear garbage collector - info.reader = nil - - // Store boundaries. - info.BSize = bodyInfo.Size - path := stringPathFromInts(currentPath) - (*bs)[path] = info - - // Fix start of subsections. - newPath := getChildPath(currentPath) - shift := info.Size - info.BSize - subInfo, err := bs.getInfo(newPath) - - // If it has subparts. - for err == nil { - subInfo.Start += shift - - // Level down. - subInfo, err = bs.getInfo(append(newPath, 1)) - if err == nil { - newPath = append(newPath, 1) - continue - } - - // Next. - newPath[len(newPath)-1]++ - subInfo, err = bs.getInfo(newPath) - if err == nil { - continue - } - - // Level up. - for { - newPath = newPath[:len(newPath)-1] - if len(newPath) > 0 { - newPath[len(newPath)-1]++ - subInfo, err = bs.getInfo(newPath) - if err != nil { - err = nil - continue - } - } - break - } - - // The end. - if len(newPath) == 0 { - break - } - } - - return nil -} - -// getChildPath will return the first child path of parent path. -// NOTE: Return value can be used to iterate over parts so it is necessary to -// copy parrent values in order to not rewrite values in parent. -func getChildPath(parent []int) []int { - // append alloc inline is the fasted way to copy - return append(append(make([]int, 0, len(parent)+1), parent...), 1) -} - -func stringPathFromInts(ints []int) (ret string) { - for i, n := range ints { - if i != 0 { - ret += "." - } - ret += strconv.Itoa(n) - } - return -} - -func (bs *BodyStructure) hasInfo(sectionPath []int) bool { - _, err := bs.getInfo(sectionPath) - return err == nil -} - -func (bs *BodyStructure) getInfoCheckSection(sectionPath []int) (sectionInfo *SectionInfo, err error) { - if len(*bs) == 1 && len(sectionPath) == 1 && sectionPath[0] == 1 { - sectionPath = []int{} - } - return bs.getInfo(sectionPath) -} - -func (bs *BodyStructure) getInfo(sectionPath []int) (sectionInfo *SectionInfo, err error) { - path := stringPathFromInts(sectionPath) - sectionInfo, ok := (*bs)[path] - if !ok { - err = errors.New("wrong section " + path) - } - return -} - -// GetSection returns bytes of section including MIME header. -func (bs *BodyStructure) GetSection(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) { - info, err := bs.getInfoCheckSection(sectionPath) - if err != nil { - return - } - return goToOffsetAndReadNBytes(wholeMail, info.Start, info.Size) -} - -// GetSectionContent returns bytes of section content (excluding MIME header). -func (bs *BodyStructure) GetSectionContent(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) { - info, err := bs.getInfoCheckSection(sectionPath) - if err != nil { - return - } - return goToOffsetAndReadNBytes(wholeMail, info.Start+info.Size-info.BSize, info.BSize) -} - -// GetMailHeader returns the main header of mail. -func (bs *BodyStructure) GetMailHeader() (header textproto.MIMEHeader, err error) { - return bs.GetSectionHeader([]int{}) -} - -// GetMailHeaderBytes returns the bytes with main mail header. -// Warning: It can contain extra lines. -func (bs *BodyStructure) GetMailHeaderBytes() (header []byte, err error) { - return bs.GetSectionHeaderBytes([]int{}) -} - -func goToOffsetAndReadNBytes(wholeMail io.ReadSeeker, offset, length int) ([]byte, error) { - if length == 0 { - return []byte{}, nil - } - if length < 0 { - return nil, errors.New("requested negative length") - } - if offset > 0 { - if _, err := wholeMail.Seek(int64(offset), io.SeekStart); err != nil { - return nil, err - } - } - out := make([]byte, length) - _, err := wholeMail.Read(out) - return out, err -} - -// GetSectionHeader returns the mime header of specified section. -func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (textproto.MIMEHeader, error) { - info, err := bs.getInfoCheckSection(sectionPath) - if err != nil { - return nil, err - } - return info.GetMIMEHeader() -} - -// GetSectionHeaderBytes returns raw header bytes of specified section. -func (bs *BodyStructure) GetSectionHeaderBytes(sectionPath []int) ([]byte, error) { - info, err := bs.getInfoCheckSection(sectionPath) - if err != nil { - return nil, err - } - return info.Header, nil -} - -// IMAPBodyStructure will prepare imap bodystructure recurently for given part. -// Use empty path to create whole email structure. -func (bs *BodyStructure) IMAPBodyStructure(currentPart []int) (imapBS *imap.BodyStructure, err error) { - var info *SectionInfo - if info, err = bs.getInfo(currentPart); err != nil { - return - } - - tpHeader, err := info.GetMIMEHeader() - if err != nil { - return - } - - mediaType, params, _ := pmmime.ParseMediaType(tpHeader.Get("Content-Type")) - - mediaTypeSep := strings.Split(mediaType, "/") - - // If it is empty or missing it will not crash. - mediaTypeSep = append(mediaTypeSep, "") - - imapBS = &imap.BodyStructure{ - MIMEType: mediaTypeSep[0], - MIMESubType: mediaTypeSep[1], - Params: params, - Size: uint32(info.BSize), - Lines: uint32(info.Lines), - } - - if val := tpHeader.Get("Content-ID"); val != "" { - imapBS.Id = val - } - - if val := tpHeader.Get("Content-Transfer-Encoding"); val != "" { - imapBS.Encoding = val - } - - if val := tpHeader.Get("Content-Description"); val != "" { - imapBS.Description = val - } - - if val := tpHeader.Get("Content-Disposition"); val != "" { - imapBS.Disposition = val - } - - nextPart := append(currentPart, 1) //nolint:gocritic - for { - if !bs.hasInfo(nextPart) { - break - } - var subStruct *imap.BodyStructure - subStruct, err = bs.IMAPBodyStructure(nextPart) - if err != nil { - return - } - if imapBS.Parts == nil { - imapBS.Parts = []*imap.BodyStructure{} - } - imapBS.Parts = append(imapBS.Parts, subStruct) - nextPart[len(nextPart)-1]++ - } - - return imapBS, nil -} diff --git a/pkg/message/section_test.go b/pkg/message/section_test.go deleted file mode 100644 index 9e15ad70..00000000 --- a/pkg/message/section_test.go +++ /dev/null @@ -1,599 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "runtime" - "sort" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -var enableDebug = false //nolint:global - -func debug(msg string, v ...interface{}) { - if !enableDebug { - return - } - - _, file, line, _ := runtime.Caller(1) - fmt.Printf("%s:%d: \033[2;33m"+msg+"\033[0;39m\n", append([]interface{}{filepath.Base(file), line}, v...)...) -} - -func TestParseBodyStructure(t *testing.T) { - expectedStructure := map[string]string{ - "": "multipart/mixed; boundary=\"0000MAIN\"", - "1": "text/plain", - "2": "application/octet-stream", - "3": "message/rfc822; boundary=\"0003MSG\"", - "3.1": "text/plain", - "3.2": "application/octet-stream", - "4": "multipart/mixed; boundary=\"0004ATTACH\"", - "4.1": "image/gif", - "4.2": "message/rfc822; boundary=\"0042MSG\"", - "4.2.1": "text/plain", - "4.2.2": "multipart/alternative; boundary=\"0422ALTER\"", - "4.2.2.1": "text/plain", - "4.2.2.2": "text/html", - } - mailReader := strings.NewReader(sampleMail) - bs, err := NewBodyStructure(mailReader) - require.NoError(t, err) - - paths := []string{} - for path := range *bs { - paths = append(paths, path) - } - sort.Strings(paths) - - debug("%10s: %-50s %5s %5s %5s %5s", "section", "type", "start", "size", "bsize", "lines") - for _, path := range paths { - sec := (*bs)[path] - header, err := sec.GetMIMEHeader() - require.NoError(t, err) - contentType := header.Get("Content-Type") - debug("%10s: %-50s %5d %5d %5d %5d", path, contentType, sec.Start, sec.Size, sec.BSize, sec.Lines) - require.Equal(t, expectedStructure[path], contentType) - } - - require.True(t, len(*bs) == len(expectedStructure), "Wrong number of sections expected %d but have %d", len(expectedStructure), len(*bs)) -} - -func TestParseBodyStructurePGP(t *testing.T) { - expectedStructure := map[string]string{ - "": "multipart/signed; micalg=pgp-sha256; protocol=\"application/pgp-signature\"; boundary=\"MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM\"", - "1": "multipart/mixed; boundary=\"FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378\"; protected-headers=\"v1\"", - "1.1": "multipart/mixed; boundary=\"------------F97C8ED4878E94675762AE43\"", - "1.1.1": "multipart/alternative; boundary=\"------------041318B15DD3FA540FED32C6\"", - "1.1.1.1": "text/plain; charset=utf-8; format=flowed", - "1.1.1.2": "text/html; charset=utf-8", - "1.1.2": "application/pdf; name=\"minimal.pdf\"", - "1.1.3": "application/pgp-keys; name=\"OpenPGP_0x161C0875822359F7.asc\"", - "2": "application/pgp-signature; name=\"OpenPGP_signature.asc\"", - } - - b, err := os.ReadFile("testdata/enc-body-structure.eml") - require.NoError(t, err) - - bs, err := NewBodyStructure(bytes.NewReader(b)) - require.NoError(t, err) - - haveStructure := map[string]string{} - for path := range *bs { - header, err := (*bs)[path].GetMIMEHeader() - require.NoError(t, err) - haveStructure[path] = header.Get("Content-Type") - } - - require.Equal(t, expectedStructure, haveStructure) -} - -func TestGetSection(t *testing.T) { - structReader := strings.NewReader(sampleMail) - bs, err := NewBodyStructure(structReader) - require.NoError(t, err) - - // Bad paths - wantPaths := [][]int{{0}, {-1}, {3, 2, 3}} - for _, wantPath := range wantPaths { - _, err = bs.getInfo(wantPath) - require.Error(t, err, "path %v", wantPath) - } - - // Whole section. - for _, try := range testPaths { - mailReader := strings.NewReader(sampleMail) - info, err := bs.getInfo(try.path) - require.NoError(t, err) - section, err := bs.GetSection(mailReader, try.path) - require.NoError(t, err) - - debug("section %v: %d %d\n___\n%s\n‾‾‾\n", try.path, info.Start, info.Size, string(section)) - - require.True(t, string(section) == try.expectedSection, "not same as expected:\n___\n%s\n‾‾‾", try.expectedSection) - } - // Body content. - for _, try := range testPaths { - mailReader := strings.NewReader(sampleMail) - info, err := bs.getInfo(try.path) - require.NoError(t, err) - section, err := bs.GetSectionContent(mailReader, try.path) - require.NoError(t, err) - - debug("content %v: %d %d\n___\n%s\n‾‾‾\n", try.path, info.Start+info.Size-info.BSize, info.BSize, string(section)) - - require.True(t, string(section) == try.expectedBody, "not same as expected:\n___\n%s\n‾‾‾", try.expectedBody) - } -} - -func TestGetSecionNoMIMEParts(t *testing.T) { - wantBody := "This is just a simple mail with no multipart structure.\n" - wantHeader := `Subject: Sample mail -From: John Doe -To: Mary Smith -Date: Fri, 21 Nov 1997 09:55:06 -0600 -Content-Type: plain/text - -` - wantMail := wantHeader + wantBody - - r := require.New(t) - bs, err := NewBodyStructure(strings.NewReader(wantMail)) - r.NoError(err) - - // Bad parts - wantPaths := [][]int{{0}, {2}, {1, 2, 3}} - for _, wantPath := range wantPaths { - _, err = bs.getInfoCheckSection(wantPath) - r.Error(err, "path %v: %d %d\n__\n%s\n", wantPath) - } - - debug := func(wantPath []int, info *SectionInfo, section []byte) string { - if info == nil { - info = &SectionInfo{} - } - return fmt.Sprintf("path %v %q: %d %d\n___\n%s\n‾‾‾\n", - wantPath, stringPathFromInts(wantPath), info.Start, info.Size, - string(section), - ) - } - - // Ok Parts - wantPaths = [][]int{{}, {1}} - for _, p := range wantPaths { - wantPath := append([]int{}, p...) - - info, err := bs.getInfoCheckSection(wantPath) - r.NoError(err, debug(wantPath, info, []byte{})) - - section, err := bs.GetSection(strings.NewReader(wantMail), wantPath) - r.NoError(err, debug(wantPath, info, section)) - r.Equal(wantMail, string(section), debug(wantPath, info, section)) - - haveBody, err := bs.GetSectionContent(strings.NewReader(wantMail), wantPath) - r.NoError(err, debug(wantPath, info, haveBody)) - r.Equal(wantBody, string(haveBody), debug(wantPath, info, haveBody)) - - haveHeader, err := bs.GetSectionHeaderBytes(wantPath) - r.NoError(err, debug(wantPath, info, haveHeader)) - r.Equal(wantHeader, string(haveHeader), debug(wantPath, info, haveHeader)) - } -} - -func TestGetMainHeaderBytes(t *testing.T) { - wantHeader := []byte(`Subject: Sample mail -From: John Doe -To: Mary Smith -Date: Fri, 21 Nov 1997 09:55:06 -0600 -Content-Type: multipart/mixed; boundary="0000MAIN" - -`) - - structReader := strings.NewReader(sampleMail) - bs, err := NewBodyStructure(structReader) - require.NoError(t, err) - - haveHeader, err := bs.GetMailHeaderBytes() - require.NoError(t, err) - require.Equal(t, wantHeader, haveHeader) -} - -/* Structure example: -HEADER ([RFC-2822] header of the message) -TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -1 TEXT/PLAIN -2 APPLICATION/OCTET-STREAM -3 MESSAGE/RFC822 -3.HEADER ([RFC-2822] header of the message) -3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -3.1 TEXT/PLAIN -3.2 APPLICATION/OCTET-STREAM -4 MULTIPART/MIXED -4.1 IMAGE/GIF -4.1.MIME ([MIME-IMB] header for the IMAGE/GIF) -4.2 MESSAGE/RFC822 -4.2.HEADER ([RFC-2822] header of the message) -4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -4.2.1 TEXT/PLAIN -4.2.2 MULTIPART/ALTERNATIVE -4.2.2.1 TEXT/PLAIN -4.2.2.2 TEXT/RICHTEXT -*/ - -var sampleMail = `Subject: Sample mail -From: John Doe -To: Mary Smith -Date: Fri, 21 Nov 1997 09:55:06 -0600 -Content-Type: multipart/mixed; boundary="0000MAIN" - -main summary - ---0000MAIN -Content-Type: text/plain - -1. main message - - ---0000MAIN -Content-Type: application/octet-stream -Content-Disposition: inline; filename="main_signature.sig" -Content-Transfer-Encoding: base64 - -2/MainOctetStream - ---0000MAIN -Subject: Inside mail 3 -From: Mary Smith -To: John Doe -Date: Fri, 20 Nov 1997 09:55:06 -0600 -Content-Type: message/rfc822; boundary="0003MSG" - -3. message summary - ---0003MSG -Content-Type: text/plain - -3.1 message text - ---0003MSG -Content-Type: application/octet-stream -Content-Disposition: attachment; filename="msg_3_signature.sig" -Content-Transfer-Encoding: base64 - -3/2/MessageOctestStream/== - ---0003MSG-- - ---0000MAIN -Content-Type: multipart/mixed; boundary="0004ATTACH" - -4 attach summary - ---0004ATTACH -Content-Type: image/gif -Content-Disposition: attachment; filename="att4.1_gif.sig" -Content-Transfer-Encoding: base64 - -4/1/Gif= - ---0004ATTACH -Subject: Inside mail 4.2 -From: Mary Smith -To: John Doe -Date: Fri, 10 Nov 1997 09:55:06 -0600 -Content-Type: message/rfc822; boundary="0042MSG" - -4.2 message summary - ---0042MSG -Content-Type: text/plain - -4.2.1 message text - ---0042MSG -Content-Type: multipart/alternative; boundary="0422ALTER" - -4.2.2 alternative summary - ---0422ALTER -Content-Type: text/plain - -4.2.2.1 plain text - ---0422ALTER -Content-Type: text/html - -

4.2.2.2 html text

- ---0422ALTER-- - ---0042MSG-- - ---0004ATTACH-- - ---0000MAIN-- - - -` - -var testPaths = []struct { - path []int - expectedSection, expectedBody string -}{ - { - []int{}, - sampleMail, - `main summary - ---0000MAIN -Content-Type: text/plain - -1. main message - - ---0000MAIN -Content-Type: application/octet-stream -Content-Disposition: inline; filename="main_signature.sig" -Content-Transfer-Encoding: base64 - -2/MainOctetStream - ---0000MAIN -Subject: Inside mail 3 -From: Mary Smith -To: John Doe -Date: Fri, 20 Nov 1997 09:55:06 -0600 -Content-Type: message/rfc822; boundary="0003MSG" - -3. message summary - ---0003MSG -Content-Type: text/plain - -3.1 message text - ---0003MSG -Content-Type: application/octet-stream -Content-Disposition: attachment; filename="msg_3_signature.sig" -Content-Transfer-Encoding: base64 - -3/2/MessageOctestStream/== - ---0003MSG-- - ---0000MAIN -Content-Type: multipart/mixed; boundary="0004ATTACH" - -4 attach summary - ---0004ATTACH -Content-Type: image/gif -Content-Disposition: attachment; filename="att4.1_gif.sig" -Content-Transfer-Encoding: base64 - -4/1/Gif= - ---0004ATTACH -Subject: Inside mail 4.2 -From: Mary Smith -To: John Doe -Date: Fri, 10 Nov 1997 09:55:06 -0600 -Content-Type: message/rfc822; boundary="0042MSG" - -4.2 message summary - ---0042MSG -Content-Type: text/plain - -4.2.1 message text - ---0042MSG -Content-Type: multipart/alternative; boundary="0422ALTER" - -4.2.2 alternative summary - ---0422ALTER -Content-Type: text/plain - -4.2.2.1 plain text - ---0422ALTER -Content-Type: text/html - -

4.2.2.2 html text

- ---0422ALTER-- - ---0042MSG-- - ---0004ATTACH-- - ---0000MAIN-- - - -`, - }, - - { - []int{1}, - `Content-Type: text/plain - -1. main message - - -`, - `1. main message - - -`, - }, - { - []int{3}, - `Subject: Inside mail 3 -From: Mary Smith -To: John Doe -Date: Fri, 20 Nov 1997 09:55:06 -0600 -Content-Type: message/rfc822; boundary="0003MSG" - -3. message summary - ---0003MSG -Content-Type: text/plain - -3.1 message text - ---0003MSG -Content-Type: application/octet-stream -Content-Disposition: attachment; filename="msg_3_signature.sig" -Content-Transfer-Encoding: base64 - -3/2/MessageOctestStream/== - ---0003MSG-- - -`, - `3. message summary - ---0003MSG -Content-Type: text/plain - -3.1 message text - ---0003MSG -Content-Type: application/octet-stream -Content-Disposition: attachment; filename="msg_3_signature.sig" -Content-Transfer-Encoding: base64 - -3/2/MessageOctestStream/== - ---0003MSG-- - -`, - }, - { - []int{3, 1}, - `Content-Type: text/plain - -3.1 message text - -`, - `3.1 message text - -`, - }, - { - []int{3, 2}, - `Content-Type: application/octet-stream -Content-Disposition: attachment; filename="msg_3_signature.sig" -Content-Transfer-Encoding: base64 - -3/2/MessageOctestStream/== - -`, - `3/2/MessageOctestStream/== - -`, - }, - { - []int{4, 2, 2, 1}, - `Content-Type: text/plain - -4.2.2.1 plain text - -`, - `4.2.2.1 plain text - -`, - }, - { - []int{4, 2, 2, 2}, - `Content-Type: text/html - -

4.2.2.2 html text

- -`, - `

4.2.2.2 html text

- -`, - }, -} - -func TestBodyStructureSerialize(t *testing.T) { - r := require.New(t) - want := &BodyStructure{ - "1": { - Header: []byte("Content: type"), - Start: 1, - Size: 2, - BSize: 3, - Lines: 4, - }, - "1.1.1": { - Header: []byte("X-Pm-Key: id"), - Start: 11, - Size: 12, - BSize: 13, - Lines: 14, - reader: bytes.NewBuffer([]byte("this should not be serialized")), - }, - } - - raw, err := want.Serialize() - r.NoError(err) - have, err := DeserializeBodyStructure(raw) - r.NoError(err) - - // Before compare remove reader (should not be serialized) - (*want)["1.1.1"].reader = nil - r.Equal(want, have) -} - -func TestSectionInfoReadHeader(t *testing.T) { - r := require.New(t) - - testData := []struct { - wantHeader, mail string - }{ - { - "key1: val1\nkey2: val2\n\n", - "key1: val1\nkey2: val2\n\nbody is here\n\nand it is not confused", - }, - { - "key1:\n val1\n\n", - "key1:\n val1\n\nbody is here", - }, - { - "key1: val1\r\nkey2: val2\r\n\r\n", - "key1: val1\r\nkey2: val2\r\n\r\nbody is here\r\n\r\nand it is not confused", - }, - } - - for _, td := range testData { - bs, err := NewBodyStructure(strings.NewReader(td.mail)) - r.NoError(err, "case %q", td.mail) - haveHeader, err := bs.GetMailHeaderBytes() - r.NoError(err, "case %q", td.mail) - r.Equal(td.wantHeader, string(haveHeader), "case %q", td.mail) - } -} diff --git a/pkg/message/writer.go b/pkg/message/writer.go deleted file mode 100644 index e1411023..00000000 --- a/pkg/message/writer.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package message - -import ( - "fmt" - "io" -) - -type partWriter struct { - w io.Writer - boundary string -} - -func newPartWriter(w io.Writer, boundary string) *partWriter { - return &partWriter{w: w, boundary: boundary} -} - -func (w *partWriter) createPart(fn func(io.Writer) error) error { - if _, err := fmt.Fprintf(w.w, "\r\n--%v\r\n", w.boundary); err != nil { - return err - } - - return fn(w.w) -} - -func (w *partWriter) done() error { - if _, err := fmt.Fprintf(w.w, "\r\n--%v--\r\n", w.boundary); err != nil { - return err - } - - return nil -} diff --git a/pkg/parallel/parallel.go b/pkg/parallel/parallel.go deleted file mode 100644 index 20825a3d..00000000 --- a/pkg/parallel/parallel.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package parallel - -import ( - "sync" - "time" -) - -// parallelJob is to be used for passing items between input, worker and -// collector. `idx` is there to know the original order. -type parallelJob struct { - idx int - value interface{} -} - -// RunParallel starts `workers` number of workers and feeds them with `input` data. -// Each worker calls `process`. Processed data is collected in the same order as -// the input and is passed in order to the `collect` callback. If an error -// occurs, the execution is stopped and the error returned. -// runParallel blocks until everything is done. -func RunParallel( //nolint:funlen - workers int, - input []interface{}, - process func(interface{}) (interface{}, error), - collect func(int, interface{}) error, -) (resultError error) { - wgProcess := &sync.WaitGroup{} - wgCollect := &sync.WaitGroup{} - - // Optimise by not executing the code at all if there is no input - // or run less workers than requested if there are few inputs. - inputLen := len(input) - if inputLen == 0 { - return nil - } - if inputLen < workers { - workers = inputLen - } - - inputChan := make(chan *parallelJob) - outputChan := make(chan *parallelJob) - - orderedCollectLock := &sync.Mutex{} - orderedCollect := make(map[int]interface{}) - - // Feed input channel used by workers with input data with index for ordering. - go func() { - defer close(inputChan) - for idx, item := range input { - if resultError != nil { - break - } - inputChan <- ¶llelJob{idx, item} - } - }() - - // Start workers and process all the inputs. - wgProcess.Add(workers) - for i := 0; i < workers; i++ { - go func() { - defer wgProcess.Done() - for item := range inputChan { - if output, err := process(item.value); err != nil { - resultError = err - break - } else { - outputChan <- ¶llelJob{item.idx, output} - } - } - }() - } - - // Collect data into map with the original position in the array. - wgCollect.Add(1) - go func() { - defer wgCollect.Done() - for output := range outputChan { - orderedCollectLock.Lock() - orderedCollect[output.idx] = output.value - orderedCollectLock.Unlock() - } - }() - - // Collect data in the same order as in the input array. - wgCollect.Add(1) - go func() { - defer wgCollect.Done() - idx := 0 - for { - if idx >= inputLen || resultError != nil { - break - } - orderedCollectLock.Lock() - value, ok := orderedCollect[idx] - if ok { - if err := collect(idx, value); err != nil { - resultError = err - } - delete(orderedCollect, idx) - idx++ - } - orderedCollectLock.Unlock() - if !ok { - time.Sleep(10 * time.Millisecond) - } - } - }() - - // When input channel is closed, all workers will finish. We need to wait - // for all of them and close the output channel only once. - wgProcess.Wait() - close(outputChan) - - // When workers are done, the last job is to finish collecting data. First - // collector is finished when output channel is closed and the second one - // when all items are passed to `collect` in the order or after an error. - wgCollect.Wait() - - return resultError -} diff --git a/pkg/parallel/parallel_test.go b/pkg/parallel/parallel_test.go deleted file mode 100644 index 99e30d83..00000000 --- a/pkg/parallel/parallel_test.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package parallel - -import ( - "errors" - "fmt" - "math" - "runtime" - "testing" - "time" - - r "github.com/stretchr/testify/require" -) - -//nolint:gochecknoglobals -var ( - testInput = []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - wantOutput = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - testProcessSleep = 100 // ms - runParallelTimeOverhead = 150 // ms - windowsCIExtra = 500 // ms - estimated experimentally -) - -func TestParallel(t *testing.T) { - workersTests := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - - for _, workers := range workersTests { - workers := workers - t.Run(fmt.Sprintf("%d", workers), func(t *testing.T) { - collected := make([]int, 0) - collect := func(idx int, value interface{}) error { - collected = append(collected, value.(int)) //nolint:forcetypeassert - return nil - } - - tstart := time.Now() - err := RunParallel(workers, testInput, processSleep, collect) - duration := time.Since(tstart) - - r.Nil(t, err) - r.Equal(t, wantOutput, collected) // Check the order is always kept. - - wantMinDuration := int(math.Ceil(float64(len(testInput))/float64(workers))) * testProcessSleep - wantMaxDuration := wantMinDuration + runParallelTimeOverhead - if runtime.GOOS == "windows" { - wantMaxDuration += windowsCIExtra - } - r.True(t, duration.Nanoseconds() > int64(wantMinDuration*1000000), "Duration too short: %v (expected: %v)", duration, wantMinDuration) - r.True(t, duration.Nanoseconds() < int64(wantMaxDuration*1000000), "Duration too long: %v (expected: %v)", duration, wantMaxDuration) - }) - } -} - -func TestParallelEmptyInput(t *testing.T) { - workersTests := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - - for _, workers := range workersTests { - workers := workers - t.Run(fmt.Sprintf("%d", workers), func(t *testing.T) { - err := RunParallel(workers, []interface{}{}, processSleep, collectNil) - r.Nil(t, err) - }) - } -} - -func TestParallelErrorInProcess(t *testing.T) { - workersTests := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - - for _, workers := range workersTests { - workers := workers - t.Run(fmt.Sprintf("%d", workers), func(t *testing.T) { - var lastCollected int - process := func(value interface{}) (interface{}, error) { - time.Sleep(10 * time.Millisecond) - if value.(int) == 5 { //nolint:forcetypeassert - return nil, errors.New("Error") - } - return value, nil - } - collect := func(idx int, value interface{}) error { - lastCollected = value.(int) //nolint:forcetypeassert - return nil - } - - err := RunParallel(workers, testInput, process, collect) - r.EqualError(t, err, "Error") - - time.Sleep(10 * time.Millisecond) - r.True(t, lastCollected < 5, "Last collected cannot be higher that 5, got: %d", lastCollected) - }) - } -} - -func TestParallelErrorInCollect(t *testing.T) { - workersTests := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - - for _, workers := range workersTests { - workers := workers - t.Run(fmt.Sprintf("%d", workers), func(t *testing.T) { - collect := func(idx int, value interface{}) error { - if value.(int) == 5 { //nolint:forcetypeassert - return errors.New("Error") - } - return nil - } - - err := RunParallel(workers, testInput, processSleep, collect) - r.EqualError(t, err, "Error") - }) - } -} - -func processSleep(value interface{}) (interface{}, error) { - time.Sleep(time.Duration(testProcessSleep) * time.Millisecond) - return value.(int), nil //nolint:forcetypeassert -} - -func collectNil(idx int, value interface{}) error { - return nil -} diff --git a/pkg/pchan/pchan.go b/pkg/pchan/pchan.go deleted file mode 100644 index 1abc9750..00000000 --- a/pkg/pchan/pchan.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pchan - -import ( - "sort" - "sync" -) - -type PChan struct { - lock sync.Mutex - items []*Item - ready, done chan struct{} - once sync.Once -} - -type Item struct { - ch *PChan - val interface{} - prio int - done sync.WaitGroup -} - -func (item *Item) Wait() { - item.done.Wait() -} - -func (item *Item) GetPriority() int { - item.ch.lock.Lock() - defer item.ch.lock.Unlock() - - return item.prio -} - -func (item *Item) SetPriority(priority int) { - item.ch.lock.Lock() - defer item.ch.lock.Unlock() - - item.prio = priority - - sort.Slice(item.ch.items, func(i, j int) bool { - return item.ch.items[i].prio < item.ch.items[j].prio - }) -} - -func New() *PChan { - return &PChan{ - ready: make(chan struct{}), - done: make(chan struct{}), - } -} - -func (ch *PChan) Push(val interface{}, prio int) *Item { - defer ch.notify() - - return ch.push(val, prio) -} - -func (ch *PChan) Pop() (interface{}, int, bool) { - select { - case <-ch.ready: - val, prio := ch.pop() - return val, prio, true - - case <-ch.done: - return nil, 0, false - } -} - -func (ch *PChan) Close() { - ch.once.Do(func() { close(ch.done) }) -} - -func (ch *PChan) push(val interface{}, prio int) *Item { - ch.lock.Lock() - defer ch.lock.Unlock() - - item := &Item{ - ch: ch, - val: val, - prio: prio, - } - - item.done.Add(1) - - ch.items = append(ch.items, item) - - return item -} - -func (ch *PChan) pop() (interface{}, int) { - ch.lock.Lock() - defer ch.lock.Unlock() - - sort.Slice(ch.items, func(i, j int) bool { - return ch.items[i].prio < ch.items[j].prio - }) - - var item *Item - - item, ch.items = ch.items[len(ch.items)-1], ch.items[:len(ch.items)-1] - - defer item.done.Done() - - return item.val, item.prio -} - -func (ch *PChan) notify() { - go func() { ch.ready <- struct{}{} }() -} diff --git a/pkg/pchan/pchan_test.go b/pkg/pchan/pchan_test.go deleted file mode 100644 index b84639e3..00000000 --- a/pkg/pchan/pchan_test.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pchan - -import ( - "sort" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPChanConcurrentPush(t *testing.T) { - ch := New() - - var wg sync.WaitGroup - - // We are going to test with 5 additional goroutines. - wg.Add(5) - - // Start 5 concurrent pushes. - go func() { defer wg.Done(); ch.Push(1, 1) }() - go func() { defer wg.Done(); ch.Push(2, 2) }() - go func() { defer wg.Done(); ch.Push(3, 3) }() - go func() { defer wg.Done(); ch.Push(4, 4) }() - go func() { defer wg.Done(); ch.Push(5, 5) }() - - // Wait for the items to be pushed. - wg.Wait() - - // All 5 should now be ready for popping. - require.Len(t, ch.items, 5) - - // They should be popped in priority order. - assert.Equal(t, 5, getValue(t, ch)) - assert.Equal(t, 4, getValue(t, ch)) - assert.Equal(t, 3, getValue(t, ch)) - assert.Equal(t, 2, getValue(t, ch)) - assert.Equal(t, 1, getValue(t, ch)) -} - -func TestPChanConcurrentPop(t *testing.T) { - ch := New() - - var wg sync.WaitGroup - - // We are going to test with 5 additional goroutines. - wg.Add(5) - - // Make a list to store the results in. - var res list - - // Start 5 concurrent pops; these consume any items pushed. - go func() { defer wg.Done(); res.append(getValue(t, ch)) }() - go func() { defer wg.Done(); res.append(getValue(t, ch)) }() - go func() { defer wg.Done(); res.append(getValue(t, ch)) }() - go func() { defer wg.Done(); res.append(getValue(t, ch)) }() - go func() { defer wg.Done(); res.append(getValue(t, ch)) }() - - // Push and block; items should be popped immediately by the waiting goroutines. - ch.Push(1, 1).Wait() - ch.Push(2, 2).Wait() - ch.Push(3, 3).Wait() - ch.Push(4, 4).Wait() - ch.Push(5, 5).Wait() - - // Wait for all items to be popped then close the result channel. - wg.Wait() - - assert.True(t, sort.IntsAreSorted(res.items)) -} - -func TestPChanClose(t *testing.T) { - ch := New() - - go ch.Push(1, 1) - - valOpen, _, okOpen := ch.Pop() - assert.True(t, okOpen) - assert.Equal(t, 1, valOpen) - - ch.Close() - - valClose, _, okClose := ch.Pop() - assert.False(t, okClose) - assert.Nil(t, valClose) -} - -type list struct { - items []int - mut sync.Mutex -} - -func (l *list) append(val int) { - l.mut.Lock() - defer l.mut.Unlock() - - l.items = append(l.items, val) -} - -func getValue(t *testing.T, ch *PChan) int { - val, _, ok := ch.Pop() - - assert.True(t, ok) - - return val.(int) //nolint:forcetypeassert -} diff --git a/pkg/pmapi/addresses.go b/pkg/pmapi/addresses.go deleted file mode 100644 index 6cf7b83e..00000000 --- a/pkg/pmapi/addresses.go +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "strings" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/go-resty/resty/v2" - "github.com/pkg/errors" -) - -// Address statuses. -const ( - DisabledAddress = iota - EnabledAddress -) - -// Address HasKeys values. -const ( - MissingKeys = iota - KeysPresent -) - -// Address types. -const ( - _ = iota // Skip first. - OriginalAddress - AliasAddress - CustomAddress - PremiumAddress -) - -// Address Send values. -const ( - NoSendAddress = iota - MainSendAddress - SecondarySendAddress -) - -// Address represents a user's address. -type Address struct { - ID string - DomainID string - Email string - Send int - Receive Boolean - Status int - Order int `json:",omitempty"` - Type int - DisplayName string - Signature string - MemberID string `json:",omitempty"` - MemberName string `json:",omitempty"` - - HasKeys int - Keys PMKeys -} - -// AddressList is a list of addresses. -type AddressList []*Address - -// ByID returns an address by id. Returns nil if no address is found. -func (l AddressList) ByID(id string) *Address { - for _, addr := range l { - if addr.ID == id { - return addr - } - } - return nil -} - -// AllEmails returns all emails. -func (l AddressList) AllEmails() (addresses []string) { - for _, a := range l { - addresses = append(addresses, a.Email) - } - return -} - -// ActiveEmails returns only active emails. -func (l AddressList) ActiveEmails() (addresses []string) { - for _, a := range l { - if a.Receive { - addresses = append(addresses, a.Email) - } - } - return -} - -// Main gets the main address. -func (l AddressList) Main() *Address { - for _, addr := range l { - if addr.Order == 1 { - return addr - } - } - return nil -} - -// ByEmail gets an address by email. Returns nil if no address is found. -func (l AddressList) ByEmail(email string) *Address { - email = SanitizeEmail(email) - for _, addr := range l { - if strings.EqualFold(addr.Email, email) { - return addr - } - } - return nil -} - -func SanitizeEmail(email string) string { - splitAt := strings.Split(email, "@") - if len(splitAt) != 2 { - return email - } - splitPlus := strings.Split(splitAt[0], "+") - email = splitPlus[0] + "@" + splitAt[1] - return email -} - -func ConstructAddress(headerEmail string, addressEmail string) string { - splitAtHeader := strings.Split(headerEmail, "@") - if len(splitAtHeader) != 2 { - return addressEmail - } - - splitPlus := strings.Split(splitAtHeader[0], "+") - if len(splitPlus) != 2 { - return addressEmail - } - - splitAtAddress := strings.Split(addressEmail, "@") - if len(splitAtAddress) != 2 { - return addressEmail - } - - return splitAtAddress[0] + "+" + splitPlus[1] + "@" + splitAtAddress[1] -} - -// GetAddresses requests all of current user addresses (without pagination). -func (c *client) GetAddresses(ctx context.Context) (addresses AddressList, err error) { - var res struct { - Addresses []*Address - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&res).Get("/addresses") - }); err != nil { - return nil, err - } - - return res.Addresses, nil -} - -func (c *client) ReorderAddresses(ctx context.Context, addressIDs []string) error { - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(&struct { - AddressIDs []string - }{ - AddressIDs: addressIDs, - }).Put("/addresses/order") - }); err != nil { - return err - } - - _, err := c.UpdateUser(ctx) - return err -} - -// Addresses returns the addresses stored in the client object itself rather than fetching from the API. -func (c *client) Addresses() AddressList { - return c.addresses -} - -// unlockAddresses unlocks all keys for all addresses of current user. -func (c *client) unlockAddress(passphrase []byte, address *Address) error { - if address == nil { - return errors.New("address data is missing") - } - - if address.HasKeys == MissingKeys { - return nil - } - - kr, err := address.Keys.UnlockAll(passphrase, c.userKeyRing) - if err != nil { - return errors.Wrap(err, "cannot unlock address keys for "+address.ID) - } - - c.addrKeyRing[address.ID] = kr - return nil -} - -func (c *client) KeyRingForAddressID(addrID string) (*crypto.KeyRing, error) { - if kr, ok := c.addrKeyRing[addrID]; ok { - return kr, nil - } - - return nil, errors.New("no keyring available") -} diff --git a/pkg/pmapi/addresses_test.go b/pkg/pmapi/addresses_test.go deleted file mode 100644 index f3bd252b..00000000 --- a/pkg/pmapi/addresses_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "net/http" - "testing" - - r "github.com/stretchr/testify/require" -) - -var testAddressList = AddressList{ - &Address{ - ID: "1", - Email: "root@nsa.gov", - Send: SecondarySendAddress, - Status: EnabledAddress, - Order: 2, - }, - &Address{ - ID: "2", - Email: "root@gchq.gov.uk", - Send: MainSendAddress, - Status: EnabledAddress, - Order: 1, - }, - &Address{ - ID: "3", - Email: "root@protonmail.com", - Send: NoSendAddress, - Status: DisabledAddress, - Order: 3, - }, -} - -func routeGetAddresses(tb testing.TB, w http.ResponseWriter, req *http.Request) string { - r.NoError(tb, checkMethodAndPath(req, "GET", "/addresses")) - r.NoError(tb, isAuthReq(req, testUID, testAccessToken)) - return "addresses/get_response.json" -} - -func TestAddressList(t *testing.T) { - input := "1" - addr := testAddressList.ByID(input) - r.Equal(t, testAddressList[0], addr) - - input = "42" - addr = testAddressList.ByID(input) - r.Nil(t, addr) - - input = "root@protonmail.com" - addr = testAddressList.ByEmail(input) - r.Equal(t, testAddressList[2], addr) - - input = "idontexist@protonmail.com" - addr = testAddressList.ByEmail(input) - r.Nil(t, addr) - - addr = testAddressList.Main() - r.Equal(t, testAddressList[1], addr) -} diff --git a/pkg/pmapi/attachments.go b/pkg/pmapi/attachments.go deleted file mode 100644 index f7fda857..00000000 --- a/pkg/pmapi/attachments.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/textproto" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/go-resty/resty/v2" -) - -type header textproto.MIMEHeader - -type rawHeader map[string]json.RawMessage - -func (h *header) UnmarshalJSON(b []byte) error { - if *h == nil { - *h = make(header) - } - - raw := make(rawHeader) - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - - for k, v := range raw { - // Most headers are string because they have only one value. - var s string - if err := json.Unmarshal(v, &s); err == nil { - textproto.MIMEHeader(*h).Set(k, s) - continue - } - - // If it's not a string, it must be an array of strings. - var a []string - if err := json.Unmarshal(v, &a); err != nil { - return fmt.Errorf("pmapi: attachment header field is neither a string nor an array of strings: %v", err) - } - for _, vv := range a { - textproto.MIMEHeader(*h).Add(k, vv) - } - } - - return nil -} - -const ( - DispositionInline = "inline" - DispositionAttachment = "attachment" -) - -// Attachment represents a message attachment. -type Attachment struct { - ID string `json:",omitempty"` - MessageID string `json:",omitempty"` // msg v3 ??? - Name string `json:",omitempty"` - Size int64 `json:",omitempty"` - MIMEType string `json:",omitempty"` - ContentID string `json:",omitempty"` - Disposition string - KeyPackets string `json:",omitempty"` - Signature string `json:",omitempty"` - - Header textproto.MIMEHeader `json:"-"` -} - -// Define a new type to prevent MarshalJSON/UnmarshalJSON infinite loops. -type attachment Attachment - -type rawAttachment struct { - attachment - - Header header `json:"Headers,omitempty"` -} - -func (a *Attachment) MarshalJSON() ([]byte, error) { - var raw rawAttachment - raw.attachment = attachment(*a) - - if a.Header != nil { - raw.Header = header(a.Header) - } - - return json.Marshal(&raw) -} - -func (a *Attachment) UnmarshalJSON(b []byte) error { - var raw rawAttachment - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - - *a = Attachment(raw.attachment) - - if raw.Header != nil { - a.Header = textproto.MIMEHeader(raw.Header) - } - - return nil -} - -// Decrypt decrypts this attachment's data from r using the keys from kr. -func (a *Attachment) Decrypt(r io.Reader, kr *crypto.KeyRing) (decrypted io.Reader, err error) { - keyPackets, err := base64.StdEncoding.DecodeString(a.KeyPackets) - if err != nil { - return - } - return decryptAttachment(kr, keyPackets, r) -} - -// Encrypt encrypts an attachment. -func (a *Attachment) Encrypt(kr *crypto.KeyRing, att io.Reader) (encrypted io.Reader, err error) { - return encryptAttachment(kr, att, a.Name) -} - -func (a *Attachment) DetachedSign(kr *crypto.KeyRing, att io.Reader) (signed io.Reader, err error) { - return signAttachment(kr, att) -} - -// CreateAttachment uploads an attachment. It must be already encrypted and contain a MessageID. -// -// The returned created attachment contains the new attachment ID and its size. -func (c *client) CreateAttachment(ctx context.Context, att *Attachment, attData io.Reader, sigData io.Reader) (*Attachment, error) { - var res struct { - Attachment *Attachment - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&res). - SetMultipartFormData(map[string]string{ - "Filename": att.Name, - "MessageID": att.MessageID, - "MIMEType": att.MIMEType, - "ContentID": att.ContentID, - }). - SetMultipartField("DataPacket", "DataPacket.pgp", "application/octet-stream", attData). - SetMultipartField("Signature", "Signature.pgp", "application/octet-stream", sigData). - Post("/mail/v4/attachments") - }); err != nil { - return nil, err - } - - return res.Attachment, nil -} - -// GetAttachment gets an attachment's content. The returned data is encrypted. -func (c *client) GetAttachment(ctx context.Context, attachmentID string) (att io.ReadCloser, err error) { - res, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetDoNotParseResponse(true).Get("/mail/v4/attachments/" + attachmentID) - }) - if err != nil { - return nil, err - } - - return res.RawBody(), nil -} diff --git a/pkg/pmapi/attachments_test.go b/pkg/pmapi/attachments_test.go deleted file mode 100644 index 2c5bbdc5..00000000 --- a/pkg/pmapi/attachments_test.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/textproto" - "strings" - "testing" - - pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime" - - "github.com/stretchr/testify/require" -) - -const testAttachmentCleartext = `cc, -dille. -` - -// Attachment cleartext encrypted with testPrivateKeyRing. -const testKeyPacket = `wcBMA0fcZ7XLgmf2AQf/cHhfDRM9zlIuBi+h2W6DKjbbyIHMkgF6ER3JEvn/tSruUH8KTGt0N7Z+a80FFMCuXn1Y1I/nW7MVrNhGuJZAF4OymD8ugvuoAMIQX0eCYEpPXzRIWJBZg82AuowmFMsv8Dgvq4bTZq4cttI3CZcxKUNXuAearmNpmgplUKWj5USmRXK4iGB3VFGjidXkxbElrP4fD5A/rfEZ5aJgCsegqcXxX3MEjWXi9pFzgd/9phOvl1ZFm9U9hNoVAW3QsgmVeihnKaDZUyf2Qsigij21QKAUxw9U3y89eTUIqZAcmIgqeDujA3RWBgJwjtY/lOyhEmkf3AWKzehvf1xtJmCWDg==` -const testDataPacket = `0ksB6S4f4l8C1NB8yzmd/jNi0xqEZsyTDLdTP+N4Qxh3NZjla+yGRvC9rGmoUL7XVyowsG/GKTf2LXF/5E5FkX/3WMYwIv1n11ExyAE=` - -var testAttachment = &Attachment{ - ID: "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw==", - Name: "croutonmail.txt", - Size: 77, - MIMEType: "text/plain", - KeyPackets: testKeyPacket, - - Header: textproto.MIMEHeader{ - "Content-Description": {"You'll never believe what's in this text file"}, - "X-Mailer": {"Microsoft Outlook 15.0", "Microsoft Live Mail 42.0"}, - }, - MessageID: "h3CD-DT7rLoAw1vmpcajvIPAl-wwDfXR2MHtWID3wuQURDBKTiGUAwd6E2WBbS44QQKeXImW-axm6X0hAfcVCA==", -} - -// Part of GET /mail/messages/{id} response from server. -const testAttachmentJSON = `{ - "ID": "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw==", - "Name": "croutonmail.txt", - "Size": 77, - "MIMEType": "text/plain", - "KeyPackets": "` + testKeyPacket + `", - "Headers": { - "content-description": "You'll never believe what's in this text file", - "x-mailer": [ - "Microsoft Outlook 15.0", - "Microsoft Live Mail 42.0" - ] - } -} -` - -// POST /mail/attachment/ response from server. -const testCreatedAttachmentBody = `{ - "Code": 1000, - "Attachment": {"ID": "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw=="} -}` - -func TestAttachment_UnmarshalJSON(t *testing.T) { - r := require.New(t) - att := new(Attachment) - err := json.Unmarshal([]byte(testAttachmentJSON), att) - r.NoError(err) - - att.MessageID = testAttachment.MessageID // This isn't in the server response - - r.Equal(testAttachment, att) -} - -func TestClient_CreateAttachment(t *testing.T) { - r := require.New(t) - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(checkMethodAndPath(req, "POST", "/mail/v4/attachments")) - - contentType, params, err := pmmime.ParseMediaType(req.Header.Get("Content-Type")) - r.NoError(err) - r.Equal("multipart/form-data", contentType) - - mr := multipart.NewReader(req.Body, params["boundary"]) - form, err := mr.ReadForm(10 * 1024) - r.NoError(err) - defer r.NoError(form.RemoveAll()) - - r.Equal(testAttachment.Name, form.Value["Filename"][0]) - r.Equal(testAttachment.MessageID, form.Value["MessageID"][0]) - r.Equal(testAttachment.MIMEType, form.Value["MIMEType"][0]) - - dataFile, err := form.File["DataPacket"][0].Open() - r.NoError(err) - defer r.NoError(dataFile.Close()) - - b, err := io.ReadAll(dataFile) - r.NoError(err) - r.Equal(testAttachmentCleartext, string(b)) - - w.Header().Set("Content-Type", "application/json") - - fmt.Fprint(w, testCreatedAttachmentBody) - })) - defer s.Close() - - reader := strings.NewReader(testAttachmentCleartext) // In reality, this thing is encrypted - created, err := c.CreateAttachment(context.Background(), testAttachment, reader, strings.NewReader("")) - r.NoError(err) - - r.Equal(testAttachment.ID, created.ID) -} - -func TestClient_GetAttachment(t *testing.T) { - r := require.New(t) - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(checkMethodAndPath(req, "GET", "/mail/v4/attachments/"+testAttachment.ID)) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testAttachmentCleartext) - })) - defer s.Close() - - att, err := c.GetAttachment(context.Background(), testAttachment.ID) - r.NoError(err) - defer att.Close() //nolint:errcheck - - // In reality, r contains encrypted data - b, err := io.ReadAll(att) - r.NoError(err) - - r.Equal(testAttachmentCleartext, string(b)) -} - -func TestAttachmentDecrypt(t *testing.T) { - r := require.New(t) - - rawKeyPacket, err := base64.StdEncoding.DecodeString(testKeyPacket) - r.NoError(err) - - rawDataPacket, err := base64.StdEncoding.DecodeString(testDataPacket) - r.NoError(err) - - decryptAndCheck(r, bytes.NewBuffer(append(rawKeyPacket, rawDataPacket...))) -} - -func TestAttachmentEncrypt(t *testing.T) { - r := require.New(t) - - encryptedReader, err := testAttachment.Encrypt( - testPublicKeyRing, - bytes.NewBufferString(testAttachmentCleartext), - ) - r.NoError(err) - - // The result is always different due to session key. The best way is to - // test result of encryption by decrypting again acn coparet to cleartext. - decryptAndCheck(r, encryptedReader) -} - -func decryptAndCheck(r *require.Assertions, data io.Reader) { - // First separate KeyPacket from encrypted data. In our case keypacket - // has 271 bytes. - raw, err := io.ReadAll(data) - r.NoError(err) - rawKeyPacket := raw[:271] - rawDataPacket := raw[271:] - - // KeyPacket is retrieve by get GET /mail/messages/{id} - haveAttachment := &Attachment{ - KeyPackets: base64.StdEncoding.EncodeToString(rawKeyPacket), - } - - // DataPacket is received from GET /mail/attachments/{id} - decryptedReader, err := haveAttachment.Decrypt(bytes.NewBuffer(rawDataPacket), testPrivateKeyRing) - r.NoError(err) - - b, err := io.ReadAll(decryptedReader) - r.NoError(err) - - r.Equal(testAttachmentCleartext, string(b)) -} diff --git a/pkg/pmapi/auth.go b/pkg/pmapi/auth.go deleted file mode 100644 index cff12d03..00000000 --- a/pkg/pmapi/auth.go +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "crypto/rand" - "encoding/base64" - "errors" - "io" - "net/http" - "time" - - "github.com/go-resty/resty/v2" -) - -type AuthModulus struct { - Modulus string - ModulusID string -} - -type GetAuthInfoReq struct { - Username string -} - -type AuthInfo struct { - Version int - Modulus string - ServerEphemeral string - Salt string - SRPSession string -} - -type TwoFAInfo struct { - Enabled TwoFAStatus -} - -func (twoFAInfo TwoFAInfo) hasTwoFactor() bool { - return twoFAInfo.Enabled > TwoFADisabled -} - -type TwoFAStatus int - -const ( - TwoFADisabled TwoFAStatus = iota - TOTPEnabled - U2FEnabled - TOTPAndU2FEnabled -) - -type PasswordMode int - -const ( - OnePasswordMode PasswordMode = iota + 1 - TwoPasswordMode -) - -type AuthReq struct { - Username string - ClientProof string - ClientEphemeral string - SRPSession string -} - -type AuthRefresh struct { - UID string - AccessToken string - RefreshToken string - ExpiresIn int64 - Scopes []string -} - -type Auth struct { - AuthRefresh - - UserID string - ServerProof string - PasswordMode PasswordMode - TwoFA *TwoFAInfo `json:"2FA,omitempty"` -} - -func (a Auth) HasTwoFactor() bool { - if a.TwoFA == nil { - return false - } - return a.TwoFA.hasTwoFactor() -} - -func (a Auth) HasMailboxPassword() bool { - return a.PasswordMode == TwoPasswordMode -} - -type auth2FAReq struct { - TwoFactorCode string -} - -type authRefreshReq struct { - UID string - RefreshToken string - ResponseType string - GrantType string - RedirectURI string - State string -} - -func (c *client) Auth2FA(ctx context.Context, twoFactorCode string) error { - // 2FA is called during login procedure during which refresh token should - // be valid, therefore, no refresh is needed if there is an error. - ctx = ContextWithoutAuthRefresh(ctx) - - if res, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(auth2FAReq{TwoFactorCode: twoFactorCode}).Post("/auth/2fa") - }); err != nil { - if res != nil { - switch res.StatusCode() { - case http.StatusUnauthorized: - return ErrBad2FACode - case http.StatusUnprocessableEntity: - return ErrBad2FACodeTryAgain - } - } - return err - } - - return nil -} - -func (c *client) AuthDelete(ctx context.Context) error { - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.Delete("/auth") - }); err != nil { - return err - } - - c.uid, c.acc, c.ref, c.exp = "", "", "", time.Time{} - c.sendAuthRefresh(nil) - return nil -} - -func (c *client) AuthSalt(ctx context.Context) (string, error) { - salts, err := c.GetKeySalts(ctx) - if err != nil { - return "", err - } - - if _, err := c.CurrentUser(ctx); err != nil { - return "", err - } - - for _, s := range salts { - if s.ID == c.user.Keys[0].ID { - return s.KeySalt, nil - } - } - - return "", errors.New("no matching salt found") -} - -func (c *client) AddAuthRefreshHandler(handler AuthRefreshHandler) { - c.authHandlers = append(c.authHandlers, handler) -} - -func (c *client) authRefresh(ctx context.Context) error { - c.authLocker.Lock() - defer c.authLocker.Unlock() - - if c.ref == "" { - return ErrUnauthorized - } - - auth, err := c.manager.authRefresh(ctx, c.uid, c.ref) - if err != nil { - if IsFailedAuth(err) { - c.sendAuthRefresh(nil) - } - return err - } - - c.acc = auth.AccessToken - c.ref = auth.RefreshToken - c.exp = expiresIn(auth.ExpiresIn) - - c.sendAuthRefresh(auth) - return nil -} - -func (c *client) sendAuthRefresh(auth *AuthRefresh) { - for _, handler := range c.authHandlers { - go handler(auth) - } - if auth == nil { - c.authHandlers = []AuthRefreshHandler{} - } -} - -func randomString(length int) string { - noise := make([]byte, length) - - if _, err := io.ReadFull(rand.Reader, noise); err != nil { - panic(err) - } - - return base64.StdEncoding.EncodeToString(noise)[:length] -} - -func (c *client) GetCurrentAuth() *Auth { - return &Auth{ - UserID: c.user.ID, - AuthRefresh: AuthRefresh{ - UID: c.uid, - RefreshToken: c.ref, - }, - } -} diff --git a/pkg/pmapi/auth_server_test.go b/pkg/pmapi/auth_server_test.go deleted file mode 100644 index 1353ae19..00000000 --- a/pkg/pmapi/auth_server_test.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - - "github.com/stretchr/testify/require" -) - -type testRefreshResponse struct { - Code int - AccessToken string - ExpiresIn int - TokenType string - Scope string - Scopes []string - UID string - RefreshToken string - LocalID int - - r *require.Assertions -} - -var tokenID = 0 - -func newTestRefreshToken(r *require.Assertions) testRefreshResponse { - tokenID++ - scopes := []string{ - "full", - "self", - "parent", - "user", - "loggedin", - "paid", - "nondelinquent", - "mail", - "verified", - } - return testRefreshResponse{ - Code: 1000, - AccessToken: fmt.Sprintf("acc%d", tokenID), - ExpiresIn: 3600, - TokenType: "Bearer", - Scope: strings.Join(scopes, " "), - Scopes: scopes, - UID: fmt.Sprintf("uid%d", tokenID), - RefreshToken: fmt.Sprintf("ref%d", tokenID), - r: r, - } -} - -func (r *testRefreshResponse) isCorrectRefreshToken(body io.ReadCloser) int { - request := authRefreshReq{} - err := json.NewDecoder(body).Decode(&request) - r.r.NoError(body.Close()) - r.r.NoError(err) - - if r.UID != request.UID { - return http.StatusUnprocessableEntity - } - if r.RefreshToken != request.RefreshToken { - return http.StatusBadRequest - } - return http.StatusOK -} - -func (r *testRefreshResponse) handleAuthRefresh(response http.ResponseWriter, request *http.Request) { - if code := r.isCorrectRefreshToken(request.Body); code != http.StatusOK { - response.WriteHeader(code) - return - } - - tokenID++ - r.AccessToken = fmt.Sprintf("acc%d", tokenID) - r.RefreshToken = fmt.Sprintf("ref%d", tokenID) - - response.Header().Set("Content-Type", "application/json") - response.WriteHeader(http.StatusOK) - r.r.NoError(json.NewEncoder(response).Encode(r)) -} - -func (r *testRefreshResponse) wantAuthRefresh() AuthRefresh { - return AuthRefresh{ - UID: r.UID, - AccessToken: r.AccessToken, - RefreshToken: r.RefreshToken, - ExpiresIn: int64(r.ExpiresIn), - Scopes: r.Scopes, - } -} - -func (r *testRefreshResponse) isAuthorized(header http.Header) bool { - return header.Get("x-pm-uid") == r.UID && header.Get("Authorization") == "Bearer "+r.AccessToken -} - -func (r *testRefreshResponse) handleAuthCheckOnly(response http.ResponseWriter, request *http.Request) { - if r.isAuthorized(request.Header) { - response.WriteHeader(http.StatusOK) - } else { - response.WriteHeader(http.StatusUnauthorized) - } -} diff --git a/pkg/pmapi/auth_test.go b/pkg/pmapi/auth_test.go deleted file mode 100644 index 944510ce..00000000 --- a/pkg/pmapi/auth_test.go +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestAutomaticAuthRefresh(t *testing.T) { - r := require.New(t) - mux := http.NewServeMux() - - currentTokens := newTestRefreshToken(r) - testUID := currentTokens.UID - testAcc := currentTokens.AccessToken - testRef := currentTokens.RefreshToken - currentTokens.ExpiresIn = 100 - - mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh) - mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly) - - ts := httptest.NewServer(mux) - - var gotAuthRefresh *AuthRefresh - - c := New(Config{HostURL: ts.URL}). - NewClient(testUID, testAcc, testRef, time.Now().Add(-time.Second)) - - c.AddAuthRefreshHandler(func(auth *AuthRefresh) { gotAuthRefresh = auth }) - - // Make a request with an access token that already expired one second ago. - _, err := c.GetAddresses(context.Background()) - r.NoError(err) - - wantAuthRefresh := currentTokens.wantAuthRefresh() - - // The auth callback should have been called. - r.NotNil(gotAuthRefresh) - r.Equal(wantAuthRefresh, *gotAuthRefresh) - - cl := c.(*client) //nolint:forcetypeassert // we want to panic here - r.Equal(wantAuthRefresh.AccessToken, cl.acc) - r.Equal(wantAuthRefresh.RefreshToken, cl.ref) - r.WithinDuration(expiresIn(100), cl.exp, time.Second) -} - -func Test401AuthRefresh(t *testing.T) { - r := require.New(t) - currentTokens := newTestRefreshToken(r) - testUID := currentTokens.UID - testRef := currentTokens.RefreshToken - - mux := http.NewServeMux() - - mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh) - mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly) - - ts := httptest.NewServer(mux) - var gotAuthRefresh *AuthRefresh - - // Create a new client. - m := New(Config{HostURL: ts.URL}) - c := m.NewClient(testUID, "oldAccToken", testRef, time.Now().Add(time.Hour)) - - // Register an auth handler. - c.AddAuthRefreshHandler(func(auth *AuthRefresh) { gotAuthRefresh = auth }) - - // The first request will fail with 401, triggering a refresh and retry. - _, err := c.GetAddresses(context.Background()) - r.NoError(err) - - // The auth callback should have been called. - r.NotNil(gotAuthRefresh) - r.Equal(currentTokens.wantAuthRefresh(), *gotAuthRefresh) -} - -func Test401RevokedAuth(t *testing.T) { - r := require.New(t) - currentTokens := newTestRefreshToken(r) - - mux := http.NewServeMux() - - mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh) - mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly) - - ts := httptest.NewServer(mux) - - c := New(Config{HostURL: ts.URL}). - NewClient("badUID", "badAcc", "badRef", time.Now().Add(time.Hour)) - - // The request will fail with 401, triggering a refresh. - // The retry will also fail with 401, returning an error. - _, err := c.GetAddresses(context.Background()) - r.True(IsFailedAuth(err)) -} - -func Test401OldRefreshToken(t *testing.T) { - r := require.New(t) - currentTokens := newTestRefreshToken(r) - - mux := http.NewServeMux() - - mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh) - mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly) - - ts := httptest.NewServer(mux) - - c := New(Config{HostURL: ts.URL}). - NewClient(currentTokens.UID, "oldAcc", "oldRef", time.Now().Add(time.Hour)) - - // The request will fail with 401, triggering a refresh. - // The retry will also fail with 401, returning an error. - _, err := c.GetAddresses(context.Background()) - r.True(IsFailedAuth(err)) -} - -func Test401NoAccessToken(t *testing.T) { - r := require.New(t) - currentTokens := newTestRefreshToken(r) - testUID := currentTokens.UID - testRef := currentTokens.RefreshToken - mux := http.NewServeMux() - mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh) - mux.HandleFunc("/addresses", currentTokens.handleAuthCheckOnly) - - ts := httptest.NewServer(mux) - - c := New(Config{HostURL: ts.URL}). - NewClient(testUID, "", testRef, time.Now().Add(time.Hour)) - - // The request will fail with 401, triggering a refresh. After the refresh it should succeed. - _, err := c.GetAddresses(context.Background()) - r.NoError(err) -} - -func Test401ExpiredAuthUpdateUser(t *testing.T) { - r := require.New(t) - mux := http.NewServeMux() - currentTokens := newTestRefreshToken(r) - testUID := currentTokens.UID - testRef := currentTokens.RefreshToken - - mux.HandleFunc("/auth/refresh", currentTokens.handleAuthRefresh) - - mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { - if !currentTokens.isAuthorized(r.Header) { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - respObj := struct { - Code int - User *User - }{ - Code: 1000, - User: &User{ - ID: "MJLke8kWh1BBvG95JBIrZvzpgsZ94hNNgjNHVyhXMiv4g9cn6SgvqiIFR5cigpml2LD_iUk_3DkV29oojTt3eA==", - Name: "jason", - UsedSpace: &usedSpace, - }, - } - if err := json.NewEncoder(w).Encode(respObj); err != nil { - panic(err) - } - }) - - mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) { - if !currentTokens.isAuthorized(r.Header) { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - respObj := struct { - Code int - Addresses []*Address - }{ - Code: 1000, - Addresses: []*Address{}, - } - if err := json.NewEncoder(w).Encode(respObj); err != nil { - panic(err) - } - }) - - ts := httptest.NewServer(mux) - m := New(Config{HostURL: ts.URL}) - c, _, err := m.NewClientWithRefresh(context.Background(), testUID, testRef) - r.NoError(err) - - // The request will fail with 401, triggering a refresh. After the refresh it should succeed. - _, err = c.UpdateUser(context.Background()) - r.NoError(err) -} - -func TestAuth2FA(t *testing.T) { - r := require.New(t) - twoFACode := "code" - - finish, c := newTestClientCallbacks(t, - func(tb testing.TB, w http.ResponseWriter, req *http.Request) string { - r.NoError(checkMethodAndPath(req, "POST", "/auth/2fa")) - - var twoFAreq auth2FAReq - r.NoError(json.NewDecoder(req.Body).Decode(&twoFAreq)) - r.Equal(twoFAreq.TwoFactorCode, twoFACode) - - return "/auth/2fa/post_response.json" - }, - ) - defer finish() - - err := c.Auth2FA(context.Background(), twoFACode) - r.NoError(err) -} - -func TestAuth2FA_Fail(t *testing.T) { - r := require.New(t) - finish, c := newTestClientCallbacks(t, - func(tb testing.TB, w http.ResponseWriter, req *http.Request) string { - r.NoError(checkMethodAndPath(req, "POST", "/auth/2fa")) - return "/auth/2fa/post_401_bad_password.json" - }, - ) - defer finish() - - err := c.Auth2FA(context.Background(), "code") - r.Equal(ErrBad2FACode, err) -} - -func TestAuth2FA_Retry(t *testing.T) { - r := require.New(t) - finish, c := newTestClientCallbacks(t, - func(tb testing.TB, w http.ResponseWriter, req *http.Request) string { - r.NoError(checkMethodAndPath(req, "POST", "/auth/2fa")) - return "/auth/2fa/post_422_bad_password.json" - }, - ) - defer finish() - - err := c.Auth2FA(context.Background(), "code") - r.Equal(ErrBad2FACodeTryAgain, err) -} diff --git a/pkg/pmapi/boolean.go b/pkg/pmapi/boolean.go deleted file mode 100644 index 6aa68827..00000000 --- a/pkg/pmapi/boolean.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import "encoding/json" - -type Boolean bool - -func (boolean *Boolean) UnmarshalJSON(b []byte) error { - var value int - err := json.Unmarshal(b, &value) - if err != nil { - return err - } - - *boolean = Boolean(value == 1) - return nil -} - -func (boolean Boolean) MarshalJSON() ([]byte, error) { - var value int - if boolean { - value = 1 - } - return json.Marshal(value) -} diff --git a/pkg/pmapi/client.go b/pkg/pmapi/client.go deleted file mode 100644 index 6a3d5add..00000000 --- a/pkg/pmapi/client.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "net/http" - "sync" - "time" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/go-resty/resty/v2" -) - -// client is a client of the protonmail API. It implements the Client interface. -type client struct { - manager clientManager - - uid, acc, ref string - authHandlers []AuthRefreshHandler - authLocker sync.RWMutex - - user *User - addresses AddressList - userKeyRing *crypto.KeyRing - addrKeyRing map[string]*crypto.KeyRing - keyRingLock sync.Locker - - exp time.Time -} - -func newClient(manager clientManager, uid string) *client { - return &client{ - manager: manager, - uid: uid, - addrKeyRing: make(map[string]*crypto.KeyRing), - keyRingLock: &sync.RWMutex{}, - } -} - -func (c *client) withAuth(acc, ref string, exp time.Time) *client { - c.acc = acc - c.ref = ref - c.exp = exp - - return c -} - -func (c *client) r(ctx context.Context) (*resty.Request, error) { - r := c.manager.r(ctx) - - if c.uid != "" { - r.SetHeader("x-pm-uid", c.uid) - } - - if time.Now().After(c.exp) { - if err := c.authRefresh(ctx); err != nil { - return nil, err - } - } - - c.authLocker.RLock() - defer c.authLocker.RUnlock() - - if c.acc != "" { - r.SetAuthToken(c.acc) - } - - return r, nil -} - -// do executes fn and may repeat execution in case of retry after "401 Unauthorized" error. -// Note: fn may be called more than once. -func (c *client) do(ctx context.Context, fn func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) { - r, err := c.r(ctx) - if err != nil { - return nil, err - } - - res, err := wrapNoConnection(fn(r)) - if err != nil { - if res.StatusCode() != http.StatusUnauthorized { - // Return also response so caller has more options to decide what to do. - return res, err - } - - if !isAuthRefreshDisabled(ctx) { - if err := c.authRefresh(ctx); err != nil { - return nil, err - } - - // We need to reconstruct request since access token is changed with authRefresh. - r, err := c.r(ctx) - if err != nil { - return nil, err - } - - return wrapNoConnection(fn(r)) - } - - return res, err - } - - return res, nil -} diff --git a/pkg/pmapi/client_keys.go b/pkg/pmapi/client_keys.go deleted file mode 100644 index 464db6ac..00000000 --- a/pkg/pmapi/client_keys.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" -) - -// Unlock unlocks all the user and address keys using the given passphrase, creating user and address keyrings. -// If the keyrings are already present, they are not recreated. -func (c *client) Unlock(ctx context.Context, passphrase []byte) (err error) { - c.keyRingLock.Lock() - defer c.keyRingLock.Unlock() - - return c.unlock(ctx, passphrase) -} - -// unlock unlocks the user's keys but without locking the keyring lock first. -// Should only be used internally by methods that first lock the lock. -func (c *client) unlock(ctx context.Context, passphrase []byte) error { - if _, err := c.CurrentUser(ctx); err != nil { - return err - } - - if c.userKeyRing == nil { - if err := c.unlockUser(passphrase); err != nil { - return ErrUnlockFailed{err} - } - } - - for _, address := range c.addresses { - if c.addrKeyRing[address.ID] == nil { - if err := c.unlockAddress(passphrase, address); err != nil { - return ErrUnlockFailed{err} - } - } - } - - return nil -} - -func (c *client) ReloadKeys(ctx context.Context, passphrase []byte) (err error) { - c.keyRingLock.Lock() - defer c.keyRingLock.Unlock() - - c.clearKeys() - - return c.unlock(ctx, passphrase) -} - -func (c *client) clearKeys() { - if c.userKeyRing != nil { - c.userKeyRing.ClearPrivateParams() - c.userKeyRing = nil - } - - for id, kr := range c.addrKeyRing { - if kr != nil { - kr.ClearPrivateParams() - } - delete(c.addrKeyRing, id) - } -} - -func (c *client) IsUnlocked() bool { - if c.userKeyRing == nil { - return false - } - - for _, address := range c.addresses { - if address.HasKeys != MissingKeys && c.addrKeyRing[address.ID] == nil { - return false - } - } - - return true -} diff --git a/pkg/pmapi/client_types.go b/pkg/pmapi/client_types.go deleted file mode 100644 index 7b12072d..00000000 --- a/pkg/pmapi/client_types.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "io" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/go-resty/resty/v2" -) - -// Client defines the interface of a PMAPI client. -type Client interface { - Auth2FA(context.Context, string) error - AuthSalt(ctx context.Context) (string, error) - AuthDelete(context.Context) error - AddAuthRefreshHandler(AuthRefreshHandler) - - GetUser(ctx context.Context) (*User, error) - CurrentUser(ctx context.Context) (*User, error) - UpdateUser(ctx context.Context) (*User, error) - Unlock(ctx context.Context, passphrase []byte) (err error) - ReloadKeys(ctx context.Context, passphrase []byte) (err error) - IsUnlocked() bool - - Addresses() AddressList - GetAddresses(context.Context) (addresses AddressList, err error) - ReorderAddresses(ctx context.Context, addressIDs []string) error - - GetEvent(ctx context.Context, eventID string) (*Event, error) - - SendMessage(context.Context, string, *SendMessageReq) (sent, parent *Message, err error) - CreateDraft(ctx context.Context, m *Message, parent string, action int) (created *Message, err error) - Import(context.Context, ImportMsgReqs) ([]*ImportMsgRes, error) - - CountMessages(ctx context.Context, addressID string) ([]*MessagesCount, error) - ListMessages(ctx context.Context, filter *MessagesFilter) ([]*Message, int, error) - GetMessage(ctx context.Context, apiID string) (*Message, error) - DeleteMessages(ctx context.Context, apiIDs []string) error - LabelMessages(ctx context.Context, apiIDs []string, labelID string) error - UnlabelMessages(ctx context.Context, apiIDs []string, labelID string) error - MarkMessagesRead(ctx context.Context, apiIDs []string) error - MarkMessagesUnread(ctx context.Context, apiIDs []string) error - - ListLabels(ctx context.Context) ([]*Label, error) - CreateLabel(ctx context.Context, label *Label) (*Label, error) - UpdateLabel(ctx context.Context, label *Label) (*Label, error) - DeleteLabel(ctx context.Context, labelID string) error - EmptyFolder(ctx context.Context, labelID string, addressID string) error - - // /core/V4/labels routes - ListLabelsOnly(ctx context.Context) ([]*Label, error) - ListFoldersOnly(ctx context.Context) ([]*Label, error) - CreateLabelV4(ctx context.Context, label *Label) (*Label, error) - UpdateLabelV4(ctx context.Context, label *Label) (*Label, error) - DeleteLabelV4(ctx context.Context, labelID string) error - - GetMailSettings(ctx context.Context) (MailSettings, error) - GetContactEmailByEmail(context.Context, string, int, int) ([]ContactEmail, error) - GetContactByID(context.Context, string) (Contact, error) - DecryptAndVerifyCards([]Card) ([]Card, error) - - GetAttachment(ctx context.Context, id string) (att io.ReadCloser, err error) - CreateAttachment(ctx context.Context, att *Attachment, r io.Reader, sig io.Reader) (created *Attachment, err error) - - GetUserKeyRing() (*crypto.KeyRing, error) - KeyRingForAddressID(string) (kr *crypto.KeyRing, err error) - GetPublicKeysForEmail(context.Context, string) ([]PublicKey, bool, error) -} - -type AuthRefreshHandler func(*AuthRefresh) - -type clientManager interface { - r(context.Context) *resty.Request - authRefresh(context.Context, string, string) (*AuthRefresh, error) - setSentryUserID(userID string) -} diff --git a/pkg/pmapi/config.go b/pkg/pmapi/config.go deleted file mode 100644 index 7fbb97d9..00000000 --- a/pkg/pmapi/config.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "runtime" - - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -type Config struct { - // HostURL is the base URL of API. - HostURL string - - // AppVersion sets version to headers of each request. - AppVersion string - - // UserAgent sets user agent to headers of each request. - // Used only if GetUserAgent is not set. - UserAgent string - - // GetUserAgent is dynamic version of UserAgent. - // Overrides UserAgent. - GetUserAgent func() string - - // UpgradeApplicationHandler is used to notify when there is a force upgrade. - UpgradeApplicationHandler func() - - // TLSIssueHandler is used to notify when there is a TLS issue. - TLSIssueHandler func() -} - -func NewConfig(appVersionName, appVersion string) Config { - return Config{ - HostURL: getRootURL(), - AppVersion: getAPIOS() + cases.Title(language.Und).String(appVersionName) + "_" + appVersion, - } -} - -func (c *Config) getUserAgent() string { - if c.GetUserAgent == nil { - return c.UserAgent - } - return c.GetUserAgent() -} - -// getAPIOS returns actual operating system. -func getAPIOS() string { - switch os := runtime.GOOS; os { - case "darwin": // nolint: const - return "macOS" - case "linux": - return "Linux" - case "windows": - return "Windows" - } - return "Linux" -} diff --git a/pkg/pmapi/config_default.go b/pkg/pmapi/config_default.go deleted file mode 100644 index 7d6841c6..00000000 --- a/pkg/pmapi/config_default.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build !build_qa -// +build !build_qa - -package pmapi - -import ( - "net/http" -) - -func getRootURL() string { - return "https://api.protonmail.ch" -} - -func newProxyDialerAndTransport(cfg Config) (*ProxyTLSDialer, http.RoundTripper) { - basicDialer := NewBasicTLSDialer(cfg) - pinningDialer := NewPinningTLSDialer(cfg, basicDialer) - proxyDialer := NewProxyTLSDialer(cfg, pinningDialer) - return proxyDialer, CreateTransportWithDialer(proxyDialer) -} diff --git a/pkg/pmapi/config_qa.go b/pkg/pmapi/config_qa.go deleted file mode 100644 index 2f00f6f5..00000000 --- a/pkg/pmapi/config_qa.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -//go:build build_qa -// +build build_qa - -package pmapi - -import ( - "crypto/tls" - "net/http" - "os" - "strings" -) - -func getRootURL() string { - // This config allows to dynamically change ROOT URL. - url := os.Getenv("PMAPI_ROOT_URL") - if strings.HasPrefix(url, "http") { - return url - } - if url != "" { - return "https://" + url - } - return "https://api.protonmail.ch" -} - -func newProxyDialerAndTransport(cfg Config) (*ProxyTLSDialer, http.RoundTripper) { - transport := CreateTransportWithDialer(NewBasicTLSDialer(cfg)) - - // TLS certificate of testing environment might be self-signed. - transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - return nil, transport -} diff --git a/pkg/pmapi/contacts.go b/pkg/pmapi/contacts.go deleted file mode 100644 index 87934f1f..00000000 --- a/pkg/pmapi/contacts.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "errors" - "strconv" - - "github.com/go-resty/resty/v2" -) - -type Card struct { - Type int - Data string - Signature string -} - -const ( - CardEncrypted = 1 - CardSigned = 2 -) - -type Contact struct { - ID string - Name string - UID string - Size int64 - CreateTime int64 - ModifyTime int64 - LabelIDs []string - - ContactEmails []ContactEmail - Cards []Card -} - -type ContactEmail struct { - ID string - Name string - Email string - Type []string - Defaults int - Order int - ContactID string - LabelIDs []string -} - -var errVerificationFailed = errors.New("signature verification failed") - -// ================= Public utility functions ====================== - -func (c *client) DecryptAndVerifyCards(cards []Card) ([]Card, error) { - for i := range cards { - card := &cards[i] - if isEncryptedCardType(card.Type) { - signedCard, err := c.decrypt(card.Data) - if err != nil { - return nil, err - } - card.Data = string(signedCard) - } - if isSignedCardType(card.Type) { - err := c.verify(card.Data, card.Signature) - if err != nil { - return cards, errVerificationFailed - } - } - } - return cards, nil -} - -// GetContactByID gets contact details specified by contact ID. -func (c *client) GetContactByID(ctx context.Context, contactID string) (contactDetail Contact, err error) { - var res struct { - Contact Contact - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&res).Get("/contacts/v4/" + contactID) - }); err != nil { - return Contact{}, err - } - - return res.Contact, nil -} - -// GetContactEmailByEmail gets all emails from all contacts matching a specified email string. -func (c *client) GetContactEmailByEmail(ctx context.Context, email string, page int, pageSize int) (contactEmails []ContactEmail, err error) { - var res struct { - ContactEmails []ContactEmail - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - r = r.SetQueryParams(map[string]string{ - "Email": email, - "Page": strconv.Itoa(page), - }) - if pageSize != 0 { - r.SetQueryParam("PageSize", strconv.Itoa(pageSize)) - } - return r.SetResult(&res).Get("/contacts/v4/emails") - }); err != nil { - return nil, err - } - - return res.ContactEmails, nil -} - -func isSignedCardType(cardType int) bool { - return (cardType & CardSigned) == CardSigned -} - -func isEncryptedCardType(cardType int) bool { - return (cardType & CardEncrypted) == CardEncrypted -} diff --git a/pkg/pmapi/contacts_test.go b/pkg/pmapi/contacts_test.go deleted file mode 100644 index 805c1fca..00000000 --- a/pkg/pmapi/contacts_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "fmt" - "net/http" - "reflect" - "testing" - - r "github.com/stretchr/testify/require" -) - -var ( - CleartextCard = 0 - EncryptedCard = 1 - SignedCard = 2 - EncryptedSignedCard = 3 -) - -var testGetContactByIDResponseBody = `{ - "Code": 1000, - "Contact": { - "ID": "s_SN9y1q0jczjYCH4zhvfOdHv1QNovKhnJ9bpDcTE0u7WCr2Z-NV9uubHXvOuRozW-HRVam6bQupVYRMC3BCqg==", - "Name": "Alice", - "UID": "proton-web-98c8de5e-4536-140b-9ab0-bd8ab6a2050b", - "Size": 243, - "CreateTime": 1517395498, - "ModifyTime": 1517395498, - "Cards": [ - { - "Type": 3, - "Data": "-----BEGIN PGP MESSAGE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwcBMA1vYAFKnBP8gAQf/RnOQRpo8DVJHQSJRgckEaUQvdMcADiM4L23diyiS\nQfclby/Ve2WInmvZc2RJ3rWENfeqyDZE6krQT642pKiW09GOIyVIjl+hje9y\nE4HBX0AIAWv7QhhKX6UZcM5dYSFbV3j3QxQB8A4Thng2G6ltotMTlbtcHbhu\n96Lt6ngA1tngXLSF5seyflnoiSQ5gLi2qVzrd95dIP6D4Ottcp929/4hDGmq\nPyxw9dColx6gVd1bmIDSI6ewkET4Grmo6QYqjSvjqLOf0PqHKzqypSFLkI5l\nmmnWKYTQCgl9wX+hq6Qz5E+m/BtbkdeX0YxYUss2e+oSAzJmnfdETErG9U5z\n3NJqAc3sgdwDzfWHBzogAxAbDHiqrF6zMlR5SFvZ6nRU7M2DTOE5dJhf+zOp\n1WSKn5LR46LGyt0m5wJPDjaGyQdPffAO4EULvwhGENe10UxRjY1qcUmjYOtS\nunl/vh3afI9PC1jj+HHJD2VgCA==\n=UpcY\n-----END PGP MESSAGE-----\n", - "Signature": "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwsBcBAEBCAAQBQJacZ4pCRDMO9BwcW4mpAAA6h0H/2+97koXzly5pu9hpbaW\n75d1Q976RjMr5DjAx6tKFtSzznel8YfWgvA6OQmMGdPY8ae7/+3mwCJZYWy/\nXVvUfCSflmYpSIKGfP+Vm1XezWY1W84DGhiFj5n8sdaWisv3bpFwFf1YR3Ae\noBoZ4ufNzaQALRqGPMgXETtXZCtzuL/+0vGSKj5SLECiRcSE4jCPEVRy2bcl\nWJyB9r4VmcjF042OMHxphXoYmTEWvgigyaQFHNORu5cK9EHfHpCG6IcjGbdx\n+9Px5YnDY1ix+YpBKePGSTlLE0u6ow0VTUrdvNjl7IUBaRcfJcIIdgCBOTMw\n1uQ/yeyP46V5AFXFnIKeZeQ=\n=FlOf\n-----END PGP SIGNATURE-----\n" - }, - { - "Type": 2, - "Data": "BEGIN:VCARD\nVERSION:4.0\nFN;TYPE=fn:Alice\nitem1.EMAIL:alice@protonmail.com\nUID:proton-web-98c8de5e-4536-140b-9ab0-bd8ab6a2050b\nEND:VCARD", - "Signature": "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwsBcBAEBCAAQBQJacZ4qCRDMO9BwcW4mpAAA3jUIAJ88mIyO8Yj0+evSFXnK\nNxNdjNgn7t1leY0BWlh1nkK76XrZEPipdw2QU8cOcZzn1Wby2SGfZVkwoPc4\nzAhPT4WKbkFVqXhDry5399kLwGYJCxdEcw/oPyYj+YgpQKMxhTrQq21tbEwr\n7JDRBXgi3Cckh/XsteFHOIiAVnM7BV6zFudipnYxa4uNF0Bf4VbUZx1Mm0Wb\nMJaGsO5reqQUQzDPO5TdSAZ8qGSdjVv7RESgUu5DckcDSsnB987Zbh9uFc22\nfPYmb6zA0cEZh3dAjpDPT7cg8hlvfYBb+kP3sLFyLiIkdEG8Pcagjf0k+l76\nr1IsPlYBx2LJmsJf+WDNlj8=\n=Xn+3\n-----END PGP SIGNATURE-----\n" - } - ], - "ContactEmails": [ - { - "ID": "4m2sBxLq4McqD0D330Kuy5xG-yyDNXyLEjG5_RYcjy9X-3qHGNP07DNOWLY40TYtUAQr4fAVp8zOcZ_z2o6H-A==", - "Name": "Alice", - "Email": "alice@protonmail.com", - "Type": [], - "Defaults": 1, - "Order": 1, - "ContactID": "s_SN9y1q0jczjYCH4zhvfOdHv1QNovKhnJ9bpDcTE0u7WCr2Z-NV9uubHXvOuRozW-HRVam6bQupVYRMC3BCqg==", - "LabelIDs": [] - } - ], - "LabelIDs": [] - } -}` - -var testGetContactEmailByEmailResponseBody = `{ - "Code": 1000, - "ContactEmails": [ - { - "ID": "aefew4323jFv0BhSMw==", - "Name": "ProtonMail Features", - "Email": "features@protonmail.black", - "Type": [ - "work" - ], - "Defaults": 1, - "Order": 1, - "ContactID": "a29olIjFv0rnXxBhSMw==", - "LabelIDs": [ - "I6hgx3Ol-d3HYa3E394T_ACXDmTaBub14w==" - ], - "CanonicalEmail": "features@protonmail.black", - "LastUsedTime": 1612546350 - } - ], - "Total": 2 -}` - -var testGetContactByID = Contact{ - ID: "s_SN9y1q0jczjYCH4zhvfOdHv1QNovKhnJ9bpDcTE0u7WCr2Z-NV9uubHXvOuRozW-HRVam6bQupVYRMC3BCqg==", - Name: "Alice", - UID: "proton-web-98c8de5e-4536-140b-9ab0-bd8ab6a2050b", - Size: 243, - CreateTime: 1517395498, - ModifyTime: 1517395498, - Cards: []Card{ - { - Type: 3, - Data: "-----BEGIN PGP MESSAGE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwcBMA1vYAFKnBP8gAQf/RnOQRpo8DVJHQSJRgckEaUQvdMcADiM4L23diyiS\nQfclby/Ve2WInmvZc2RJ3rWENfeqyDZE6krQT642pKiW09GOIyVIjl+hje9y\nE4HBX0AIAWv7QhhKX6UZcM5dYSFbV3j3QxQB8A4Thng2G6ltotMTlbtcHbhu\n96Lt6ngA1tngXLSF5seyflnoiSQ5gLi2qVzrd95dIP6D4Ottcp929/4hDGmq\nPyxw9dColx6gVd1bmIDSI6ewkET4Grmo6QYqjSvjqLOf0PqHKzqypSFLkI5l\nmmnWKYTQCgl9wX+hq6Qz5E+m/BtbkdeX0YxYUss2e+oSAzJmnfdETErG9U5z\n3NJqAc3sgdwDzfWHBzogAxAbDHiqrF6zMlR5SFvZ6nRU7M2DTOE5dJhf+zOp\n1WSKn5LR46LGyt0m5wJPDjaGyQdPffAO4EULvwhGENe10UxRjY1qcUmjYOtS\nunl/vh3afI9PC1jj+HHJD2VgCA==\n=UpcY\n-----END PGP MESSAGE-----\n", - Signature: "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwsBcBAEBCAAQBQJacZ4pCRDMO9BwcW4mpAAA6h0H/2+97koXzly5pu9hpbaW\n75d1Q976RjMr5DjAx6tKFtSzznel8YfWgvA6OQmMGdPY8ae7/+3mwCJZYWy/\nXVvUfCSflmYpSIKGfP+Vm1XezWY1W84DGhiFj5n8sdaWisv3bpFwFf1YR3Ae\noBoZ4ufNzaQALRqGPMgXETtXZCtzuL/+0vGSKj5SLECiRcSE4jCPEVRy2bcl\nWJyB9r4VmcjF042OMHxphXoYmTEWvgigyaQFHNORu5cK9EHfHpCG6IcjGbdx\n+9Px5YnDY1ix+YpBKePGSTlLE0u6ow0VTUrdvNjl7IUBaRcfJcIIdgCBOTMw\n1uQ/yeyP46V5AFXFnIKeZeQ=\n=FlOf\n-----END PGP SIGNATURE-----\n", - }, - { - Type: 2, - Data: "BEGIN:VCARD\nVERSION:4.0\nFN;TYPE=fn:Alice\nitem1.EMAIL:alice@protonmail.com\nUID:proton-web-98c8de5e-4536-140b-9ab0-bd8ab6a2050b\nEND:VCARD", - Signature: "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwsBcBAEBCAAQBQJacZ4qCRDMO9BwcW4mpAAA3jUIAJ88mIyO8Yj0+evSFXnK\nNxNdjNgn7t1leY0BWlh1nkK76XrZEPipdw2QU8cOcZzn1Wby2SGfZVkwoPc4\nzAhPT4WKbkFVqXhDry5399kLwGYJCxdEcw/oPyYj+YgpQKMxhTrQq21tbEwr\n7JDRBXgi3Cckh/XsteFHOIiAVnM7BV6zFudipnYxa4uNF0Bf4VbUZx1Mm0Wb\nMJaGsO5reqQUQzDPO5TdSAZ8qGSdjVv7RESgUu5DckcDSsnB987Zbh9uFc22\nfPYmb6zA0cEZh3dAjpDPT7cg8hlvfYBb+kP3sLFyLiIkdEG8Pcagjf0k+l76\nr1IsPlYBx2LJmsJf+WDNlj8=\n=Xn+3\n-----END PGP SIGNATURE-----\n", - }, - }, - ContactEmails: []ContactEmail{ - { - ID: "4m2sBxLq4McqD0D330Kuy5xG-yyDNXyLEjG5_RYcjy9X-3qHGNP07DNOWLY40TYtUAQr4fAVp8zOcZ_z2o6H-A==", - Name: "Alice", - Email: "alice@protonmail.com", - Type: []string{}, - Defaults: 1, - Order: 1, - ContactID: "s_SN9y1q0jczjYCH4zhvfOdHv1QNovKhnJ9bpDcTE0u7WCr2Z-NV9uubHXvOuRozW-HRVam6bQupVYRMC3BCqg==", - LabelIDs: []string{}, - }, - }, - LabelIDs: []string{}, -} - -var testGetContactEmailByEmail = []ContactEmail{ - { - ID: "aefew4323jFv0BhSMw==", - Name: "ProtonMail Features", - Email: "features@protonmail.black", - Type: []string{"work"}, - Defaults: 1, - Order: 1, - ContactID: "a29olIjFv0rnXxBhSMw==", - LabelIDs: []string{"I6hgx3Ol-d3HYa3E394T_ACXDmTaBub14w=="}, - }, -} - -func TestContact_GetContactById(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/contacts/v4/s_SN9y1q0jczjYCH4zhvfOdHv1QNovKhnJ9bpDcTE0u7WCr2Z-NV9uubHXvOuRozW-HRVam6bQupVYRMC3BCqg==")) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testGetContactByIDResponseBody) - })) - defer s.Close() - - contact, err := c.GetContactByID(context.Background(), "s_SN9y1q0jczjYCH4zhvfOdHv1QNovKhnJ9bpDcTE0u7WCr2Z-NV9uubHXvOuRozW-HRVam6bQupVYRMC3BCqg==") - r.NoError(t, err) - - if !reflect.DeepEqual(contact, testGetContactByID) { - t.Fatalf("Invalid got contact: expected %+v, got %+v", testGetContactByID, contact) - } -} - -func TestContact_GetContactEmailByEmail(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/contacts/v4/emails?Email=someone%40pm.me&Page=1&PageSize=10")) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testGetContactEmailByEmailResponseBody) - })) - defer s.Close() - - contact, err := c.GetContactEmailByEmail(context.Background(), "someone@pm.me", 1, 10) - r.NoError(t, err) - - if !reflect.DeepEqual(contact, testGetContactEmailByEmail) { - t.Fatalf("Invalid got contact: expected %+v, got %+v", testGetContactByID, contact) - } -} - -func TestContact_isSignedCardType(t *testing.T) { - if !isSignedCardType(SignedCard) || !isSignedCardType(EncryptedSignedCard) { - t.Fatal("isSignedCardType shouldn't return false for signed card types") - } - if isSignedCardType(CleartextCard) || isSignedCardType(EncryptedCard) { - t.Fatal("isSignedCardType shouldn't return true for non-signed card types") - } -} - -func TestContact_isEncryptedCardType(t *testing.T) { - if !isEncryptedCardType(EncryptedCard) || !isEncryptedCardType(EncryptedSignedCard) { - t.Fatal("isEncryptedCardType shouldn't return false for encrypted card types") - } - if isEncryptedCardType(CleartextCard) || isEncryptedCardType(SignedCard) { - t.Fatal("isEncryptedCardType shouldn't return true for non-encrypted card types") - } -} - -var testCardsEncrypted = []Card{ - { - Type: EncryptedSignedCard, - Data: "-----BEGIN PGP MESSAGE-----\nVersion: GopenPGP 0.0.1 (ddacebe0)\nComment: https://gopenpgp.org\n\nwcBMA0fcZ7XLgmf2AQf/fLKA6ZCkDxumpDoUoFQfO86B9LFuqGEJq+voP12C6UXo\nfB2nTy/K4+VosLKYOkU9sW1PZOCL+i00z+zkqUZ6jchbZBpzwy/UCTmpPRw5zrmr\nW6bZCwwgqJSGVWrvcrDA3bW9cn/HHqQqU6jNeXIF+IuhTscRAJVGehJZYWjr1lgB\nToJhg4+//Bgp/Fxzz8Fej/fsokgOlRJ8xcZKYx0rKL/+Il0u2jnd08kJTegpaY+6\nBlsYBzfYq25WkS02iy02wHbt6XD7AxFDi4WDjsM8bryLSm/KNWrejqfDYb/tMAKa\nKNJqK39/EUewzp1gHEXiGmdDEIFTKCHTDTPV84mwf9I1Ae4yoLs+ilYE6sSk7DCh\nPSWjDC8lpKzmw93slsejTG93HJKQPcZ0rLBpv6qPZX6widNYjDE=\n=QFxr\n-----END PGP MESSAGE-----", - Signature: "-----BEGIN PGP SIGNATURE-----\nVersion: GopenPGP 0.0.1 (ddacebe0)\nComment: https://gopenpgp.org\n\nwsBcBAABCgAQBQJdZQ1kCRA+tiWe3yHfJAAA9nMH/0X7pS8TGt6Ox0BewRh0vjfQ\n9LPLwbOiHdj97LNqutZcLlDTfm9SPH82221ZpVILWhB0u2kFeNUGihVbjAqJGYJn\nEk2TELLwn8csYRy9r5JkyUirqrvh7jgl4vs1yt8O/3Yb4ARudOoZr8Yrb4+NVNe0\nCcwQJnH/fJPtF1hbarKwtKtCo3IFwTis4pc8qWJRpBH61z1mO0Yr/LIh85QndhnF\nnZ/3MkWOY0kp2gl4ptqtNUw7z+JJ4LLVdT3ycdVK7GVTZmIG90y5KKxwJvrwbS7/\n8rmPGPQ5diLEMrzuKC2plXT6Pdy0ShtZxie2C3JY86e7ol7xvl0pNqxzOrj424w=\n=AOTG\n-----END PGP SIGNATURE-----", - }, -} - -var testCardsCleartext = []Card{ - { - Type: EncryptedSignedCard, - Data: "data", - Signature: "-----BEGIN PGP SIGNATURE-----\nVersion: GopenPGP 0.0.1 (ddacebe0)\nComment: https://gopenpgp.org\n\nwsBcBAABCgAQBQJdZQ1kCRA+tiWe3yHfJAAA9nMH/0X7pS8TGt6Ox0BewRh0vjfQ\n9LPLwbOiHdj97LNqutZcLlDTfm9SPH82221ZpVILWhB0u2kFeNUGihVbjAqJGYJn\nEk2TELLwn8csYRy9r5JkyUirqrvh7jgl4vs1yt8O/3Yb4ARudOoZr8Yrb4+NVNe0\nCcwQJnH/fJPtF1hbarKwtKtCo3IFwTis4pc8qWJRpBH61z1mO0Yr/LIh85QndhnF\nnZ/3MkWOY0kp2gl4ptqtNUw7z+JJ4LLVdT3ycdVK7GVTZmIG90y5KKxwJvrwbS7/\n8rmPGPQ5diLEMrzuKC2plXT6Pdy0ShtZxie2C3JY86e7ol7xvl0pNqxzOrj424w=\n=AOTG\n-----END PGP SIGNATURE-----", - }, -} - -func TestClient_Decrypt(t *testing.T) { - c := newClient(newManager(Config{}), "") - c.userKeyRing = testPrivateKeyRing - - cardCleartext, err := c.DecryptAndVerifyCards(testCardsEncrypted) - r.Nil(t, err) - r.Equal(t, testCardsCleartext[0].Data, cardCleartext[0].Data) -} diff --git a/pkg/pmapi/context.go b/pkg/pmapi/context.go deleted file mode 100644 index 74180c9e..00000000 --- a/pkg/pmapi/context.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" -) - -type pmapiContextKey string - -const ( - retryContextKey = pmapiContextKey("retry") - retryDisabled = "disabled" - - authRefreshContextKey = pmapiContextKey("authRefresh") - authRefreshDisabled = "disabled" -) - -func ContextWithoutRetry(parent context.Context) context.Context { - return context.WithValue(parent, retryContextKey, retryDisabled) -} - -func isRetryDisabled(ctx context.Context) bool { - if v := ctx.Value(retryContextKey); v != nil { - return v == retryDisabled - } - return false -} - -func ContextWithoutAuthRefresh(parent context.Context) context.Context { - return context.WithValue(parent, authRefreshContextKey, authRefreshDisabled) -} - -func isAuthRefreshDisabled(ctx context.Context) bool { - if v := ctx.Value(authRefreshContextKey); v != nil { - return v == authRefreshDisabled - } - return false -} diff --git a/pkg/pmapi/data_test.go b/pkg/pmapi/data_test.go deleted file mode 100644 index 5d5f7d9d..00000000 --- a/pkg/pmapi/data_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import "github.com/ProtonMail/gopenpgp/v2/crypto" - -var testIdentity = &crypto.Identity{ - Name: "UserID", - Email: "", -} - -const ( - testUID = "729ad6012421d67ad26950dc898bebe3a6e3caa2" //nolint:gosec - testAccessToken = "de0423049b44243afeec7d9c1d99be7b46da1e8a" //nolint:gosec - testRefreshToken = "a49b98256745bb497bec20e9b55f5de16f01fb52" //nolint:gosec -) diff --git a/pkg/pmapi/errors.go b/pkg/pmapi/errors.go deleted file mode 100644 index f67a6857..00000000 --- a/pkg/pmapi/errors.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import "errors" - -var ( - ErrNoConnection = errors.New("no internet connection") - ErrUnauthorized = errors.New("API client is unauthorized") - ErrUpgradeApplication = errors.New("application upgrade required") - - ErrBad2FACode = errors.New("incorrect 2FA code") - ErrBad2FACodeTryAgain = errors.New("incorrect 2FA code: please try again") - - ErrPaidPlanRequired = errors.New("paid subscription plan is required") - ErrPasswordWrong = errors.New("wrong password") -) - -// ErrUnprocessableEntity ... -type ErrUnprocessableEntity struct { - OriginalError error -} - -func IsUnprocessableEntity(err error) bool { - _, ok := err.(ErrUnprocessableEntity) - return ok -} - -func (err ErrUnprocessableEntity) Error() string { - return err.OriginalError.Error() -} - -// ErrBadRequest ... -type ErrBadRequest struct { - OriginalError error -} - -func IsBadRequest(err error) bool { - _, ok := err.(ErrBadRequest) - return ok -} - -func (err ErrBadRequest) Error() string { - return err.OriginalError.Error() -} - -// ErrAuthFailed ... -type ErrAuthFailed struct { - OriginalError error -} - -func IsFailedAuth(err error) bool { - _, ok := err.(ErrAuthFailed) - return ok -} - -func (err ErrAuthFailed) Error() string { - return err.OriginalError.Error() -} - -// ErrUnlockFailed ... -type ErrUnlockFailed struct { - OriginalError error -} - -func IsFailedUnlock(err error) bool { - _, ok := err.(ErrUnlockFailed) - return ok -} - -func (err ErrUnlockFailed) Error() string { - return err.OriginalError.Error() -} diff --git a/pkg/pmapi/events.go b/pkg/pmapi/events.go deleted file mode 100644 index 08f356d8..00000000 --- a/pkg/pmapi/events.go +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "encoding/json" - "net/mail" - - "github.com/go-resty/resty/v2" -) - -// Event represents changes since the last check. -type Event struct { - // The current event ID. - EventID string - // If set to one, all cached data must be fetched again. - Refresh int - // If set to one, fetch more events. - More Boolean - // Changes applied to messages. - Messages []*EventMessage - // Counts of messages per labels. - MessageCounts []*MessagesCount - // Changes applied to labels. - Labels []*EventLabel - // Current user status. - User *User - // Changes to addresses. - Addresses []*EventAddress - // Messages to show to the user. - Notices []string - - // Update of used user space - UsedSpace *int64 -} - -// EventAction is the action that created a change. -type EventAction int - -const ( - EventDelete EventAction = iota // EventDelete Item has been deleted. - EventCreate // EventCreate Item has been created. - EventUpdate // EventUpdate Item has been updated. - EventUpdateFlags // EventUpdateFlags For messages: flags have been updated. -) - -// Flags for event refresh. -const ( - EventRefreshMail = 1 - EventRefreshContact = 2 - EventRefreshAll = 255 -) - -// maxNumberOfMergedEvents limits how many events are merged into one. It means -// when GetEvent is called and event returns there is more events, it will -// automatically fetch next one and merge it up to this number of events. -const maxNumberOfMergedEvents = 50 - -// EventItem is an item that has changed. -type EventItem struct { - ID string - Action EventAction -} - -// EventMessage is a message that has changed. -type EventMessage struct { - EventItem - - // If the message has been created, the new message. - Created *Message `json:"-"` - // If the message has been updated, the updated fields. - Updated *EventMessageUpdated `json:"-"` -} - -// eventMessage defines a new type to prevent MarshalJSON/UnmarshalJSON infinite loops. -type eventMessage EventMessage - -type rawEventMessage struct { - eventMessage - - // This will be parsed depending on the action. - Message json.RawMessage `json:",omitempty"` -} - -func (em *EventMessage) UnmarshalJSON(b []byte) (err error) { - var raw rawEventMessage - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - *em = EventMessage(raw.eventMessage) - - switch em.Action { - case EventCreate: - em.Created = &Message{ID: raw.ID} - return json.Unmarshal(raw.Message, em.Created) - case EventUpdate, EventUpdateFlags: - em.Updated = &EventMessageUpdated{ID: raw.ID} - return json.Unmarshal(raw.Message, em.Updated) - case EventDelete: - return nil - } - return nil -} - -func (em *EventMessage) MarshalJSON() ([]byte, error) { - var raw rawEventMessage - raw.eventMessage = eventMessage(*em) - - var err error - switch em.Action { - case EventCreate: - raw.Message, err = json.Marshal(em.Created) - case EventUpdate, EventUpdateFlags: - raw.Message, err = json.Marshal(em.Updated) - case EventDelete: - } - if err != nil { - return nil, err - } - - return json.Marshal(raw) -} - -// EventMessageUpdated contains changed fields for an updated message. -type EventMessageUpdated struct { - ID string - - Subject *string - Unread *Boolean - Flags *int64 - Sender *mail.Address - ToList *[]*mail.Address - CCList *[]*mail.Address - BCCList *[]*mail.Address - Time int64 - - // Fields only present for EventUpdateFlags. - LabelIDs []string - LabelIDsAdded []string - LabelIDsRemoved []string -} - -// EventLabel is a label that has changed. -type EventLabel struct { - EventItem - Label *Label -} - -// EventAddress is an address that has changed. -type EventAddress struct { - EventItem - Address *Address -} - -// GetEvent returns a summary of events that occurred since last. To get the latest event, -// provide an empty last value. The latest event is always empty. -func (c *client) GetEvent(ctx context.Context, eventID string) (*Event, error) { - return c.getEvent(ctx, eventID, 1) -} - -func (c *client) getEvent(ctx context.Context, eventID string, numberOfMergedEvents int) (*Event, error) { - if eventID == "" { - eventID = "latest" - } - - var event *Event - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&event).Get("/events/" + eventID) - }); err != nil { - return nil, err - } - - // API notifies about used space two ways: - // - by `event.User.UsedSpace` - // - by `event.UsedSpace` - // - // Because event merging is implemented for User object we copy the - // value from event.UsedSpace to event.User.UsedSpace and continue with - // user. - if event.UsedSpace != nil { - if event.User == nil { - event.User = &User{UsedSpace: event.UsedSpace} - } else { - event.User.UsedSpace = event.UsedSpace - } - } - - if event.More && numberOfMergedEvents < maxNumberOfMergedEvents { - nextEvent, err := c.getEvent(ctx, event.EventID, numberOfMergedEvents+1) - if err != nil { - return nil, err - } - event = mergeEvents(event, nextEvent) - } - - return event, nil -} - -// mergeEvents combines an old events and a new events object. -// This is not as simple as just blindly joining the two because some things should only be taken from the new events. -func mergeEvents(eventsOld *Event, eventsNew *Event) (mergedEvents *Event) { - return &Event{ - EventID: eventsNew.EventID, - Refresh: eventsOld.Refresh | eventsNew.Refresh, - More: eventsNew.More, - Messages: append(eventsOld.Messages, eventsNew.Messages...), - MessageCounts: append(eventsOld.MessageCounts, eventsNew.MessageCounts...), - Labels: append(eventsOld.Labels, eventsNew.Labels...), - User: mergeUserEvents(eventsOld.User, eventsNew.User), - Addresses: append(eventsOld.Addresses, eventsNew.Addresses...), - Notices: append(eventsOld.Notices, eventsNew.Notices...), - } -} - -func mergeUserEvents(userOld, userNew *User) *User { - if userNew == nil { - return userOld - } - - if userOld != nil { - if userNew.MaxSpace == nil { - userNew.MaxSpace = userOld.MaxSpace - } - if userNew.UsedSpace == nil { - userNew.UsedSpace = userOld.UsedSpace - } - } - - return userNew -} diff --git a/pkg/pmapi/events_test.go b/pkg/pmapi/events_test.go deleted file mode 100644 index b187ebe1..00000000 --- a/pkg/pmapi/events_test.go +++ /dev/null @@ -1,538 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "fmt" - "net/http" - "net/mail" - "regexp" - "strconv" - "strings" - "testing" - - r "github.com/stretchr/testify/require" -) - -func TestClient_GetEvent(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/events/latest")) - - w.Header().Set("Content-Type", "application/json") - - fmt.Fprint(w, testEventBody) - })) - defer s.Close() - - event, err := c.GetEvent(context.Background(), "") - r.NoError(t, err) - r.Equal(t, testEvent, event) -} - -func TestClient_GetEvent_withID(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/events/"+testEvent.EventID)) - - w.Header().Set("Content-Type", "application/json") - - fmt.Fprint(w, testEventBody) - })) - defer s.Close() - - event, err := c.GetEvent(context.Background(), testEvent.EventID) - r.NoError(t, err) - r.Equal(t, testEvent, event) -} - -// We first call GetEvent with id of eventID1, which returns More=1 so we fetch with id eventID2. -func TestClient_GetEvent_mergeEvents(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - - switch req.URL.RequestURI() { - case "/events/eventID1": - r.NoError(t, checkMethodAndPath(req, "GET", "/events/eventID1")) - fmt.Fprint(w, testEventBodyMore1) - case "/events/eventID2": - r.NoError(t, checkMethodAndPath(req, "GET", "/events/eventID2")) - fmt.Fprint(w, testEventBodyMore2) - default: - t.Fail() - } - })) - defer s.Close() - - event, err := c.GetEvent(context.Background(), "eventID1") - r.NoError(t, err) - r.Equal(t, testEventMerged, event) -} - -func TestClient_GetEvent_mergeMaxNumberOfEvents(t *testing.T) { - numberOfCalls := 0 - - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - numberOfCalls++ - - re := regexp.MustCompile(`/eventID([0-9]+)`) - eventIDString := re.FindStringSubmatch(req.URL.RequestURI())[1] - eventID, err := strconv.Atoi(eventIDString) - r.NoError(t, err) - - if numberOfCalls > maxNumberOfMergedEvents*2 { - r.Fail(t, "Too many calls!") - } - - body := strings.ReplaceAll(testEventBodyMore1, "eventID2", "eventID"+strconv.Itoa(eventID+1)) - w.Header().Set("Content-Type", "application/json") - - fmt.Fprint(w, body) - })) - defer s.Close() - - event, err := c.GetEvent(context.Background(), "eventID1") - r.NoError(t, err) - r.Equal(t, maxNumberOfMergedEvents, numberOfCalls) - r.True(t, bool(event.More)) -} - -var ( - testEventMessageUpdateUnread = Boolean(false) - - testEvent = &Event{ - EventID: "eventID1", - Refresh: 0, - Messages: []*EventMessage{ - { - EventItem: EventItem{ID: "hdI7aIgUO1hFplCIcJHB0jShRVsAzS0AB75wGCaiNVeIHXLmaUnt4eJ8l7c7L6uk4g0ZdXhGWG5gfh6HHgAZnw==", Action: EventCreate}, - Created: &Message{ - ID: "hdI7aIgUO1hFplCIcJHB0jShRVsAzS0AB75wGCaiNVeIHXLmaUnt4eJ8l7c7L6uk4g0ZdXhGWG5gfh6HHgAZnw==", - Header: make(mail.Header), - Subject: "Hey there", - }, - }, - { - EventItem: EventItem{ID: "bSFLAimPSfGz2Kj0aV3l3AyXsof_Vf7sfrrMJ8ifgGJe-f2NG2eLaEGXLytjMhq9wnLMtkoZpO2uBXM4nOVa5g==", Action: EventUpdateFlags}, - Updated: &EventMessageUpdated{ - ID: "bSFLAimPSfGz2Kj0aV3l3AyXsof_Vf7sfrrMJ8ifgGJe-f2NG2eLaEGXLytjMhq9wnLMtkoZpO2uBXM4nOVa5g==", - Unread: &testEventMessageUpdateUnread, - Time: 1472391377, - LabelIDsAdded: []string{ArchiveLabel}, - LabelIDsRemoved: []string{InboxLabel}, - }, - }, - { - EventItem: EventItem{ID: "XRBMBYnSkaEJWtqFACp2kjlNc-7GjzX3SnPcOtWK4PyLG11Nhsg0uxPYjTXoClQfB-EHVDl9gE3w2PVuj93jBg==", Action: EventDelete}, - }, - }, - MessageCounts: []*MessagesCount{ - { - LabelID: "0", - Total: 19, - Unread: 2, - }, - { - LabelID: "6", - Total: 1, - Unread: 0, - }, - }, - Notices: []string{"Server will be down in 2min because of a NSA attack"}, - } - - testEventMerged = &Event{ - EventID: "eventID3", - Refresh: 1, - Messages: []*EventMessage{ - { - EventItem: EventItem{ID: "msgID1", Action: EventCreate}, - Created: &Message{ - ID: "id", - Header: make(mail.Header), - Subject: "Hey there", - }, - }, - { - EventItem: EventItem{ID: "msgID2", Action: EventCreate}, - Created: &Message{ - ID: "id", - Header: make(mail.Header), - Subject: "Hey there again", - }, - }, - }, - MessageCounts: []*MessagesCount{ - { - LabelID: "label1", - Total: 19, - Unread: 2, - }, - { - LabelID: "label2", - Total: 1, - Unread: 0, - }, - { - LabelID: "label2", - Total: 2, - Unread: 1, - }, - { - LabelID: "label3", - Total: 1, - Unread: 0, - }, - }, - Notices: []string{"Server will be down in 2min because of a NSA attack", "Just kidding lol"}, - Labels: []*EventLabel{ - { - EventItem: EventItem{ - ID: "labelID1", - Action: 1, - }, - Label: &Label{ - ID: "id", - Name: "Event Label 1", - }, - }, - { - EventItem: EventItem{ - ID: "labelID2", - Action: 1, - }, - Label: &Label{ - ID: "id", - Name: "Event Label 2", - }, - }, - }, - User: &User{ - ID: "userID1", - Name: "user", - UsedSpace: &usedSpace, - MaxSpace: &maxSpace, - }, - Addresses: []*EventAddress{ - { - EventItem: EventItem{ - ID: "addressID1", - Action: 2, - }, - Address: &Address{ - ID: "id", - DisplayName: "address 1", - }, - }, - { - EventItem: EventItem{ - ID: "addressID2", - Action: 2, - }, - Address: &Address{ - ID: "id", - DisplayName: "address 2", - }, - }, - }, - } -) - -const ( - testEventBody = `{ - "EventID": "eventID1", - "Refresh": 0, - "Messages": [ - { - "ID": "hdI7aIgUO1hFplCIcJHB0jShRVsAzS0AB75wGCaiNVeIHXLmaUnt4eJ8l7c7L6uk4g0ZdXhGWG5gfh6HHgAZnw==", - "Action": 1, - "Message": { - "ID": "hdI7aIgUO1hFplCIcJHB0jShRVsAzS0AB75wGCaiNVeIHXLmaUnt4eJ8l7c7L6uk4g0ZdXhGWG5gfh6HHgAZnw==", - "Subject": "Hey there" - } - }, - { - "ID": "bSFLAimPSfGz2Kj0aV3l3AyXsof_Vf7sfrrMJ8ifgGJe-f2NG2eLaEGXLytjMhq9wnLMtkoZpO2uBXM4nOVa5g==", - "Action": 3, - "Message": { - "ConversationID": "2oX3EILYRuZ0IRBVlzMg1oV5eazQL67sFIHlcR8bjickPn7K4id4sJZuAB6n0pdtI3hRIVsjCpgWfRm8c_x3IQ==", - "Unread": 0, - "Time": 1472391377, - "Location": 6, - "LabelIDsAdded": [ - "6" - ], - "LabelIDsRemoved": [ - "0" - ] - } - }, - { - "ID": "XRBMBYnSkaEJWtqFACp2kjlNc-7GjzX3SnPcOtWK4PyLG11Nhsg0uxPYjTXoClQfB-EHVDl9gE3w2PVuj93jBg==", - "Action": 0 - } - ], - "Conversations": [ - { - "ID": "2oX3EILYRuZ0IRBVlzMg1oV5eazQL67sFIHlcR8bjickPn7K4id4sJZuAB6n0pdtI3hRIVsjCpgWfRm8c_x3IQ==", - "Action": 1, - "Conversation": { - "ID": "2oX3EILYRuZ0IRBVlzMg1oV5eazQL67sFIHlcR8bjickPn7K4id4sJZuAB6n0pdtI3hRIVsjCpgWfRm8c_x3IQ==", - "Order": 1616, - "Subject": "Hey there", - "Senders": [ - { - "Address": "apple@protonmail.com", - "Name": "apple@protonmail.com" - } - ], - "Recipients": [ - { - "Address": "apple@protonmail.com", - "Name": "apple@protonmail.com" - } - ], - "NumMessages": 1, - "NumUnread": 1, - "NumAttachments": 0, - "ExpirationTime": 0, - "TotalSize": 636, - "AddressID": "QMJs2dzTx7uqpH5PNgIzjULywU4gO9uMBhEMVFOAVJOoUml54gC0CCHtW9qYwzH-zYbZwMv3MFYncPjW1Usq7Q==", - "LabelIDs": [ - "0" - ], - "Labels": [ - { - "Count": 1, - "NumMessages": 1, - "NumUnread": 1, - "ID": "0" - } - ] - } - } - ], - "Total": { - "Locations": [ - { - "Location": 0, - "Count": 19 - }, - { - "Location": 1, - "Count": 16 - }, - { - "Location": 2, - "Count": 16 - }, - { - "Location": 3, - "Count": 17 - }, - { - "Location": 6, - "Count": 1 - } - ], - "Labels": [ - { - "LabelID": "LLz8ysmVxwr4dF6mWpClePT0SpSWOEvzTdq17RydSl4ndMckvY1K63HeXDzn03BJQwKYvgf-eWT8Qfd9WVuIEQ==", - "Count": 2 - }, - { - "LabelID": "BvbqbySUPo9uWW_eR8tLA13NUsQMz3P4Zhw4UnpvrKqURnrHlE6L2Au0nplHfHlVXFgGz4L4hJ9-BYllOL-L5g==", - "Count": 2 - } - ], - "Starred": 3 - }, - "Unread": { - "Locations": [ - { - "Location": 0, - "Count": 2 - }, - { - "Location": 1, - "Count": 0 - }, - { - "Location": 2, - "Count": 0 - }, - { - "Location": 3, - "Count": 0 - }, - { - "Location": 6, - "Count": 0 - } - ], - "Labels": [ - { - "LabelID": "LLz8ysmVxwr4dF6mWpClePT0SpSWOEvzTdq17RydSl4ndMckvY1K63HeXDzn03BJQwKYvgf-eWT8Qfd9WVuIEQ==", - "Count": 0 - }, - { - "LabelID": "BvbqbySUPo9uWW_eR8tLA13NUsQMz3P4Zhw4UnpvrKqURnrHlE6L2Au0nplHfHlVXFgGz4L4hJ9-BYllOL-L5g==", - "Count": 0 - } - ], - "Starred": 0 - }, - "MessageCounts": [ - { - "LabelID": "0", - "Total": 19, - "Unread": 2 - }, - { - "LabelID": "6", - "Total": 1, - "Unread": 0 - } - ], - "ConversationCounts": [ - { - "LabelID": "0", - "Total": 19, - "Unread": 2 - }, - { - "LabelID": "6", - "Total": 1, - "Unread": 0 - } - ], - "Notices": ["Server will be down in 2min because of a NSA attack"], - "Code": 1000 -} -` - - testEventBodyMore1 = `{ - "EventID": "eventID2", - "More": 1, - "Refresh": 1, - "Messages": [ - { - "ID": "msgID1", - "Action": 1, - "Message": { - "ID": "id", - "Subject": "Hey there" - } - } - ], - "MessageCounts": [ - { - "LabelID": "label1", - "Total": 19, - "Unread": 2 - }, - { - "LabelID": "label2", - "Total": 1, - "Unread": 0 - } - ], - "Labels": [ - { - "ID":"labelID1", - "Action":1, - "Label":{ - "ID":"id", - "Name":"Event Label 1" - } - } - ], - "User": { - "ID": "userID1", - "Name": "user", - "UsedSpace": 444, - "MaxSpace": 12345678 - }, - "Addresses": [ - { - "ID": "addressID1", - "Action": 2, - "Address": { - "ID": "id", - "DisplayName": "address 1" - } - } - ], - "UsedSpace": 12345, - "Notices": ["Server will be down in 2min because of a NSA attack"] -} -` - - testEventBodyMore2 = `{ - "EventID": "eventID3", - "Refresh": 0, - "Messages": [ - { - "ID": "msgID2", - "Action": 1, - "Message": { - "ID": "id", - "Subject": "Hey there again" - } - } - ], - "MessageCounts": [ - { - "LabelID": "label2", - "Total": 2, - "Unread": 1 - }, - { - "LabelID": "label3", - "Total": 1, - "Unread": 0 - } - ], - "Labels": [ - { - "ID":"labelID2", - "Action":1, - "Label":{ - "ID":"id", - "Name":"Event Label 2" - } - } - ], - "User": { - "ID": "userID1", - "Name": "user", - "UsedSpace": 23456 - }, - "Addresses": [ - { - "ID": "addressID2", - "Action": 2, - "Address": { - "ID": "id", - "DisplayName": "address 2" - } - } - ], - "Notices": ["Just kidding lol"] -} -` -) diff --git a/pkg/pmapi/import.go b/pkg/pmapi/import.go deleted file mode 100644 index 0cb50130..00000000 --- a/pkg/pmapi/import.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "strconv" - - "github.com/go-resty/resty/v2" -) - -const ( - MaxImportMessageRequestLength = 10 - MaxImportMessageRequestSize = 25 * 1024 * 1024 // MaxImportMessageRequestSize 25 MB total limit -) - -type ImportMsgReq struct { - Metadata *ImportMetadata // Metadata about the message to import. - Message []byte // The raw RFC822 message. -} - -type ImportMsgReqs []*ImportMsgReq - -func (reqs ImportMsgReqs) buildMultipartFormData() ([]*resty.MultipartField, error) { - metadata := make(map[string]*ImportMetadata, len(reqs)) - fields := make([]*resty.MultipartField, 0, len(reqs)) - - for i, req := range reqs { - name := strconv.Itoa(i) - - metadata[name] = req.Metadata - - fields = append(fields, &resty.MultipartField{ - Param: name, - FileName: name + ".eml", - ContentType: "message/rfc822", - Reader: bytes.NewReader(req.Message), - }) - } - - b, err := json.Marshal(metadata) - if err != nil { - return nil, err - } - - fields = append(fields, &resty.MultipartField{ - Param: "Metadata", - ContentType: "application/json", - Reader: bytes.NewReader(b), - }) - - return fields, nil -} - -type ImportMetadata struct { - AddressID string - Unread Boolean // 0: read, 1: unread. - IsReplied Boolean // 1 if the message has been replied. - IsRepliedAll Boolean // 1 if the message has been replied to all. - IsForwarded Boolean // 1 if the message has been forwarded. - Time int64 // The time when the message was received as a Unix time. - Flags int64 // The type of the imported message. - LabelIDs []string // The labels to apply to the imported message. Must contain at least one system label. -} - -type ImportMsgRes struct { - // The error encountered while importing the message, if any. - Error error - // The newly created message ID. - MessageID string -} - -// Import imports messages to the user's account. -func (c *client) Import(ctx context.Context, reqs ImportMsgReqs) ([]*ImportMsgRes, error) { - if len(reqs) == 0 { - return nil, errors.New("missing import requests") - } - - if len(reqs) > MaxImportMessageRequestLength { - log. - WithField("count", len(reqs)). - Warn("Importing too many messages at once.") - return nil, errors.New("request is too long") - } - - remainingSize := MaxImportMessageRequestSize - for _, req := range reqs { - remainingSize -= len(req.Message) - if remainingSize < 0 { - log. - WithField("count", len(reqs)). - WithField("size", MaxImportMessageRequestLength-remainingSize). - Warn("Importing too big message(s)") - return nil, errors.New("request size is too big") - } - } - - fields, err := reqs.buildMultipartFormData() - if err != nil { - return nil, err - } - - var res struct { - Responses []struct { - Name string - Response struct { - Error - MessageID string - } - } - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetMultipartFields(fields...).SetResult(&res).Post("/mail/v4/messages/import") - }); err != nil { - return nil, err - } - - resps := make([]*ImportMsgRes, 0, len(res.Responses)) - - for _, resp := range res.Responses { - var err error - - if resp.Response.Code != 1000 { - err = resp.Response.Error - } - - resps = append(resps, &ImportMsgRes{ - Error: err, - MessageID: resp.Response.MessageID, - }) - } - - return resps, nil -} diff --git a/pkg/pmapi/import_test.go b/pkg/pmapi/import_test.go deleted file mode 100644 index 5bae84e2..00000000 --- a/pkg/pmapi/import_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "encoding/json" - "fmt" - "io" - "math/rand" - "mime/multipart" - "net/http" - "testing" - - pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime" - r "github.com/stretchr/testify/require" -) - -var testImportReqs = []*ImportMsgReq{ - { - Metadata: &ImportMetadata{ - AddressID: "QMJs2dzTx7uqpH5PNgIzjULywU4gO9uMBhEMVFOAVJOoUml54gC0CCHtW9qYwzH-zYbZwMv3MFYncPjW1Usq7Q==", - Unread: Boolean(false), - Flags: FlagReceived | FlagImported, - LabelIDs: []string{ArchiveLabel}, - }, - Message: []byte("Hello World!"), - }, -} - -const testImportBody = `{ - "Code": 1001, - "Responses": [{ - "Name": "0", - "Response": {"Code": 1000, "MessageID": "UKjSNz95KubYjrYmfbv1mbIfGxzY6D64mmHmVpWhkeEau-u0PIS4ru5IFMHgX6WjKpWYKCht3oiOtL5-wZChNg=="} - }] -}` - -var testImportRes = &ImportMsgRes{ - Error: nil, - MessageID: "UKjSNz95KubYjrYmfbv1mbIfGxzY6D64mmHmVpWhkeEau-u0PIS4ru5IFMHgX6WjKpWYKCht3oiOtL5-wZChNg==", -} - -func TestClient_Import(t *testing.T) { //nolint:funlen - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "POST", "/mail/v4/messages/import")) - - contentType, params, err := pmmime.ParseMediaType(req.Header.Get("Content-Type")) - r.NoError(t, err) - r.Equal(t, "multipart/form-data", contentType) - - mr := multipart.NewReader(req.Body, params["boundary"]) - - // First part is message body. - p, err := mr.NextPart() - r.NoError(t, err) - - contentDisp, params, err := pmmime.ParseMediaType(p.Header.Get("Content-Disposition")) - r.NoError(t, err) - r.Equal(t, "form-data", contentDisp) - r.Equal(t, "0", params["name"]) - - b, err := io.ReadAll(p) - r.NoError(t, err) - r.Equal(t, string(testImportReqs[0].Message), string(b)) - - // Second part is metadata. - p, err = mr.NextPart() - r.NoError(t, err) - - contentDisp, params, err = pmmime.ParseMediaType(p.Header.Get("Content-Disposition")) - r.NoError(t, err) - r.Equal(t, "form-data", contentDisp) - r.Equal(t, "Metadata", params["name"]) - - metadata := map[string]*ImportMetadata{} - err = json.NewDecoder(p).Decode(&metadata) - r.NoError(t, err) - - r.Equal(t, 1, len(metadata)) - - importReq := metadata["0"] - r.NotNil(t, req) - - expected := *testImportReqs[0].Metadata - r.Equal(t, &expected, importReq) - - // No more parts. - _, err = mr.NextPart() - r.EqualError(t, err, io.EOF.Error()) - - w.Header().Set("Content-Type", "application/json") - - fmt.Fprint(w, testImportBody) - })) - defer s.Close() - - imported, err := c.Import(context.Background(), testImportReqs) - r.NoError(t, err) - r.Equal(t, 1, len(imported)) - r.Equal(t, testImportRes, imported[0]) -} - -func TestClientImportBigSize(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.FailNow(t, "request is not dropped") - })) - defer s.Close() - - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - const size = MaxImportMessageRequestSize + 1 - msg := make([]byte, size) - for i := 0; i < size; i++ { - msg[i] = letterBytes[rand.Intn(len(letterBytes))] - } - - importRequest := []*ImportMsgReq{ - { - Metadata: &ImportMetadata{ - AddressID: "addressID", - Unread: Boolean(false), - Flags: FlagReceived | FlagImported, - LabelIDs: []string{ArchiveLabel}, - }, - Message: msg, - }, - } - - _, err := c.Import(context.Background(), importRequest) - r.EqualError(t, err, "request size is too big") -} diff --git a/pkg/pmapi/key.go b/pkg/pmapi/key.go deleted file mode 100644 index 800e1548..00000000 --- a/pkg/pmapi/key.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - - "github.com/go-resty/resty/v2" -) - -// Key flags. -const ( - UseToVerifyFlag = 1 << iota - UseToEncryptFlag -) - -type PublicKey struct { - Flags int - PublicKey string -} - -type RecipientType int - -const ( - RecipientTypeInternal RecipientType = iota + 1 - RecipientTypeExternal -) - -// GetPublicKeysForEmail returns all sending public keys for the given email address. -func (c *client) GetPublicKeysForEmail(ctx context.Context, email string) (keys []PublicKey, internal bool, err error) { - var res struct { - Keys []PublicKey - RecipientType RecipientType - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&res).SetQueryParam("Email", email).Get("/keys") - }); err != nil { - return nil, false, err - } - - return res.Keys, res.RecipientType == RecipientTypeInternal, nil -} - -// KeySalt contains id and salt for key. -type KeySalt struct { - ID, KeySalt string -} - -// GetKeySalts sends request to get list of key salts (n.b. locked route). -func (c *client) GetKeySalts(ctx context.Context) (keySalts []KeySalt, err error) { - var res struct { - KeySalts []KeySalt - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&res).Get("/keys/salts") - }); err != nil { - return nil, err - } - - return res.KeySalts, nil -} diff --git a/pkg/pmapi/keyring.go b/pkg/pmapi/keyring.go deleted file mode 100644 index e9281667..00000000 --- a/pkg/pmapi/keyring.go +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "io" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -type PMKey struct { - ID string - Version int - Flags int - Fingerprint string - PrivateKey *crypto.Key - Primary int - Token string - Active Boolean - Signature string -} - -type clearable []byte - -func (c *clearable) UnmarshalJSON(b []byte) error { - b = bytes.Trim(b, "\"") - b = bytes.ReplaceAll(b, []byte("\\n"), []byte("\n")) - b = bytes.ReplaceAll(b, []byte("\\r"), []byte("\r")) - *c = b - return nil -} - -func (c *clearable) clear() { - for i := range *c { - (*c)[i] = 0 - } -} - -func (key *PMKey) UnmarshalJSON(b []byte) (err error) { - type _PMKey PMKey - - rawKey := struct { - _PMKey - PrivateKey clearable - }{} - - defer rawKey.PrivateKey.clear() - - if err = json.Unmarshal(b, &rawKey); err != nil { - return - } - - *key = PMKey(rawKey._PMKey) - - if key.PrivateKey, err = crypto.NewKeyFromArmoredReader(bytes.NewReader(rawKey.PrivateKey)); err != nil { - return errors.Wrap(err, "failed to create crypto key from armored private key") - } - - return -} - -func (key PMKey) getPassphraseFromToken(kr *crypto.KeyRing) (passphrase []byte, err error) { - if kr == nil { - return nil, errors.New("no user key was provided") - } - - msg, err := crypto.NewPGPMessageFromArmored(key.Token) - if err != nil { - return - } - - sig, err := crypto.NewPGPSignatureFromArmored(key.Signature) - if err != nil { - return - } - - token, err := kr.Decrypt(msg, nil, 0) - if err != nil { - return - } - - if err = kr.VerifyDetached(token, sig, 0); err != nil { - return - } - - return token.GetBinary(), nil -} - -func (key PMKey) unlock(passphrase []byte) (unlockedKey *crypto.Key, err error) { - if unlockedKey, err = key.PrivateKey.Unlock(passphrase); err != nil { - return - } - - ok, err := unlockedKey.Check() - if err != nil { - return - } - if !ok { - err = errors.New("private and public keys do not match") - return - } - - return -} - -type PMKeys []PMKey - -// UnlockAll goes through each key and unlocks it, returning a keyring containing all unlocked keys, -// or an error if no keys could be unlocked. -// The passphrase is used to unlock the key unless the key's token and signature are both non-nil, -// in which case the given userkey is used to deduce the passphrase. -func (keys *PMKeys) UnlockAll(passphrase []byte, userKey *crypto.KeyRing) (kr *crypto.KeyRing, err error) { - if kr, err = crypto.NewKeyRing(nil); err != nil { - return - } - - for _, key := range *keys { - if !key.Active { - logrus.WithField("fingerprint", key.Fingerprint).Warn("Skipping inactive key") - continue - } - - var secret []byte - - if key.Token == "" || key.Signature == "" { - secret = passphrase - } else if secret, err = key.getPassphraseFromToken(userKey); err != nil { - return - } - - k, unlockErr := key.unlock(secret) - if unlockErr != nil { - logrus.WithError(unlockErr).WithField("fingerprint", key.Fingerprint).Warn("Failed to unlock key") - continue - } - - if addKeyErr := kr.AddKey(k); addKeyErr != nil { - logrus.WithError(addKeyErr).Warn("Failed to add key to keyring") - continue - } - } - - if kr.CountEntities() == 0 { - err = errors.New("no keys could be unlocked") - return - } - - return kr, err -} - -// ErrNoKeyringAvailable represents an error caused by a keyring being nil or having no entities. -var ErrNoKeyringAvailable = errors.New("no keyring available") - -func encrypt(encrypter *crypto.KeyRing, plain string, signer *crypto.KeyRing) (armored string, err error) { - if encrypter == nil { - return "", ErrNoKeyringAvailable - } - - firstKey, err := encrypter.FirstKey() - if err != nil { - return "", err - } - - plainMessage := crypto.NewPlainMessageFromString(plain) - - // We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones). - pgpMessage, err := firstKey.Encrypt(plainMessage, signer) - if err != nil { - return - } - return pgpMessage.GetArmored() -} - -func (c *client) decrypt(armored string) (plain []byte, err error) { - return decrypt(c.userKeyRing, armored) -} - -func decrypt(decrypter *crypto.KeyRing, armored string) (plainBody []byte, err error) { - if decrypter == nil { - return nil, ErrNoKeyringAvailable - } - pgpMessage, err := crypto.NewPGPMessageFromArmored(armored) - if err != nil { - return - } - plainMessage, err := decrypter.Decrypt(pgpMessage, nil, 0) - if err != nil { - return - } - return plainMessage.GetBinary(), nil -} - -func (c *client) verify(plain, amroredSignature string) (err error) { - plainMessage := crypto.NewPlainMessageFromString(plain) - pgpSignature, err := crypto.NewPGPSignatureFromArmored(amroredSignature) - if err != nil { - return - } - verifyTime := int64(0) // By default it will use current timestamp. - return c.userKeyRing.VerifyDetached(plainMessage, pgpSignature, verifyTime) -} - -func encryptAttachment(kr *crypto.KeyRing, data io.Reader, filename string) (encrypted io.Reader, err error) { - if kr == nil { - return nil, ErrNoKeyringAvailable - } - - firstKey, err := kr.FirstKey() - if err != nil { - return nil, err - } - - dataBytes, err := io.ReadAll(data) - if err != nil { - return - } - - plainMessage := crypto.NewPlainMessage(dataBytes) - - // We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones). - pgpSplitMessage, err := firstKey.EncryptAttachment(plainMessage, filename) - if err != nil { - return - } - - packets := append(pgpSplitMessage.KeyPacket, pgpSplitMessage.DataPacket...) //nolint:gocritic - - return bytes.NewReader(packets), nil -} - -func decryptAttachment(kr *crypto.KeyRing, keyPackets []byte, data io.Reader) (decrypted io.Reader, err error) { - if kr == nil { - return nil, ErrNoKeyringAvailable - } - dataBytes, err := io.ReadAll(data) - if err != nil { - return - } - pgpSplitMessage := crypto.NewPGPSplitMessage(keyPackets, dataBytes) - plainMessage, err := kr.DecryptAttachment(pgpSplitMessage) - if err != nil { - return - } - return plainMessage.NewReader(), nil -} - -func signAttachment(encrypter *crypto.KeyRing, data io.Reader) (signature io.Reader, err error) { - if encrypter == nil { - return nil, ErrNoKeyringAvailable - } - dataBytes, err := io.ReadAll(data) - if err != nil { - return - } - plainMessage := crypto.NewPlainMessage(dataBytes) - sig, err := encrypter.SignDetached(plainMessage) - if err != nil { - return - } - return bytes.NewReader(sig.GetBinary()), nil -} - -func encryptAndEncodeSessionKeys( - pubkey *crypto.KeyRing, - bodyKey *crypto.SessionKey, - attkeys map[string]*crypto.SessionKey, -) (bodyPacket string, attachmentPackets map[string]string, err error) { - // Encrypt message body keys. - packetBytes, err := pubkey.EncryptSessionKey(bodyKey) - if err != nil { - return - } - bodyPacket = base64.StdEncoding.EncodeToString(packetBytes) - - // Encrypt attachment keys. - attachmentPackets = make(map[string]string) - for id, attkey := range attkeys { - var packets []byte - if packets, err = pubkey.EncryptSessionKey(attkey); err != nil { - return - } - attachmentPackets[id] = base64.StdEncoding.EncodeToString(packets) - } - return -} - -func encryptSymmDecryptKey( - kr *crypto.KeyRing, - textToEncrypt string, -) (decryptedKey *crypto.SessionKey, symEncryptedData []byte, err error) { - // We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones). - firstKey, err := kr.FirstKey() - if err != nil { - return - } - - pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr) - if err != nil { - return - } - - pgpSplitMessage, err := pgpMessage.SplitMessage() - if err != nil { - return - } - - decryptedKey, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket()) - if err != nil { - return - } - - symEncryptedData = pgpSplitMessage.GetBinaryDataPacket() - - return -} diff --git a/pkg/pmapi/keyring_test.go b/pkg/pmapi/keyring_test.go deleted file mode 100644 index de662bf4..00000000 --- a/pkg/pmapi/keyring_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "encoding/json" - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/require" -) - -func loadPMKeys(jsonKeys string) (keys *PMKeys) { - _ = json.Unmarshal([]byte(jsonKeys), &keys) - return -} - -func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) { - r := require.New(t) - addrKeysWithTokens := loadPMKeys(readTestFile("keyring_addressKeysWithTokens_JSON", false)) - addrKeysWithoutTokens := loadPMKeys(readTestFile("keyring_addressKeysWithoutTokens_JSON", false)) - addrKeysPrimaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysPrimaryHasToken_JSON", false)) - addrKeysSecondaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysSecondaryHasToken_JSON", false)) - - key, err := crypto.NewKeyFromArmored(readTestFile("keyring_userKey", false)) - if err != nil { - panic(err) - } - - userKey, err := crypto.NewKeyRing(key) - r.NoError(err, "Expected not to receive an error unlocking user key") - - type args struct { - userKeyring *crypto.KeyRing - passphrase []byte - } - tests := []struct { - name string - keys *PMKeys - args args - }{ - { - name: "AddressKeys locked with tokens", - keys: addrKeysWithTokens, - args: args{userKeyring: userKey, passphrase: []byte("testpassphrase")}, - }, - { - name: "AddressKeys locked with passphrase, not tokens", - keys: addrKeysWithoutTokens, - args: args{userKeyring: userKey, passphrase: []byte("testpassphrase")}, - }, - { - name: "AddressKeys, primary locked with token, secondary with passphrase", - keys: addrKeysPrimaryHasToken, - args: args{userKeyring: userKey, passphrase: []byte("testpassphrase")}, - }, - { - name: "AddressKeys, primary locked with passphrase, secondary with token", - keys: addrKeysSecondaryHasToken, - args: args{userKeyring: userKey, passphrase: []byte("testpassphrase")}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - kr, err := tt.keys.UnlockAll(tt.args.passphrase, tt.args.userKeyring) //nolint:scopelint - r.NoError(err) - - // assert at least one key has been decrypted - atLeastOneDecrypted := false - - for _, k := range kr.GetKeys() { //nolint:scopelint - ok, err := k.IsUnlocked() - if err != nil { - panic(err) - } - - if ok { - atLeastOneDecrypted = true - break - } - } - - r.True(atLeastOneDecrypted) - }) - } -} - -func TestGopenpgpEncryptAttachment(t *testing.T) { - r := require.New(t) - - wantMessage := crypto.NewPlainMessage([]byte(testAttachmentCleartext)) - - pgpSplitMessage, err := testPublicKeyRing.EncryptAttachment(wantMessage, "") - r.NoError(err) - - haveMessage, err := testPrivateKeyRing.DecryptAttachment(pgpSplitMessage) - r.NoError(err) - - r.Equal(wantMessage.Data, haveMessage.Data) -} diff --git a/pkg/pmapi/labels.go b/pkg/pmapi/labels.go deleted file mode 100644 index 97b62652..00000000 --- a/pkg/pmapi/labels.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "errors" - "strconv" - - "github.com/go-resty/resty/v2" -) - -// System labels. -const ( - InboxLabel = "0" - AllDraftsLabel = "1" - AllSentLabel = "2" - TrashLabel = "3" - SpamLabel = "4" - AllMailLabel = "5" - ArchiveLabel = "6" - SentLabel = "7" - DraftLabel = "8" - StarredLabel = "10" - - LabelTypeMailBox = 1 - LabelTypeContactGroup = 2 -) - -// IsSystemLabel checks if a label is a pre-defined system label. -func IsSystemLabel(label string) bool { - switch label { - case InboxLabel, DraftLabel, SentLabel, TrashLabel, SpamLabel, ArchiveLabel, StarredLabel, AllMailLabel, AllSentLabel, AllDraftsLabel: - return true - } - return false -} - -// LabelColors provides the RGB values of the available label colors. -var LabelColors = []string{ //nolint:gochecknoglobals - "#7272a7", - "#cf5858", - "#c26cc7", - "#7569d1", - "#69a9d1", - "#5ec7b7", - "#72bb75", - "#c3d261", - "#e6c04c", - "#e6984c", - "#8989ac", - "#cf7e7e", - "#c793ca", - "#9b94d1", - "#a8c4d5", - "#97c9c1", - "#9db99f", - "#c6cd97", - "#e7d292", - "#dfb286", -} - -// Label for message. -type Label struct { //nolint:maligned - ID string - Name string - Path string - Color string - Order int `json:",omitempty"` - Display int // Not used for now, leave it empty. - Exclusive Boolean - Type int - Notify Boolean -} - -func (c *client) ListLabels(ctx context.Context) (labels []*Label, err error) { - return c.listLabelType(ctx, LabelTypeMailBox) -} - -func (c *client) ListContactGroups(ctx context.Context) (labels []*Label, err error) { - return c.listLabelType(ctx, LabelTypeContactGroup) -} - -// listLabelType lists all labels created by the user. -func (c *client) listLabelType(ctx context.Context, labelType int) (labels []*Label, err error) { - var res struct { - Labels []*Label - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetQueryParam("Type", strconv.Itoa(labelType)).SetResult(&res).Get("/labels") - }); err != nil { - return nil, err - } - - return res.Labels, nil -} - -type LabelReq struct { - *Label -} - -// CreateLabel creates a new label. -func (c *client) CreateLabel(ctx context.Context, label *Label) (created *Label, err error) { - if label.Name == "" { - return nil, errors.New("name is required") - } - - var res struct { - Label *Label - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(&LabelReq{ - Label: label, - }).SetResult(&res).Post("/labels") - }); err != nil { - return nil, err - } - - return res.Label, nil -} - -// UpdateLabel updates a label. -func (c *client) UpdateLabel(ctx context.Context, label *Label) (updated *Label, err error) { - if label.Name == "" { - return nil, errors.New("name is required") - } - - var res struct { - Label *Label - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(&LabelReq{ - Label: label, - }).SetResult(&res).Put("/labels/" + label.ID) - }); err != nil { - return nil, err - } - - return res.Label, nil -} - -// DeleteLabel deletes a label. -func (c *client) DeleteLabel(ctx context.Context, labelID string) error { - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.Delete("/labels/" + labelID) - }); err != nil { - return err - } - - return nil -} - -// LeastUsedColor is intended to return color for creating a new inbox or label. -func LeastUsedColor(colors []string) (color string) { - color = LabelColors[0] - frequency := map[string]int{} - - for _, c := range colors { - frequency[c]++ - } - - for _, c := range LabelColors { - if frequency[color] > frequency[c] { - color = c - } - } - - return -} diff --git a/pkg/pmapi/labels_test.go b/pkg/pmapi/labels_test.go deleted file mode 100644 index 8f82af8e..00000000 --- a/pkg/pmapi/labels_test.go +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" - - r "github.com/stretchr/testify/require" -) - -const testLabelsBody = `{ - "Labels": [ - { - "ID": "LLz8ysmVxwr4dF6mWpClePT0SpSWOEvzTdq17RydSl4ndMckvY1K63HeXDzn03BJQwKYvgf-eWT8Qfd9WVuIEQ==", - "Name": "CroutonMail is awesome :)", - "Color": "#7272a7", - "Display": 0, - "Order": 1, - "Type": 1 - }, - { - "ID": "BvbqbySUPo9uWW_eR8tLA13NUsQMz3P4Zhw4UnpvrKqURnrHlE6L2Au0nplHfHlVXFgGz4L4hJ9-BYllOL-L5g==", - "Name": "Royal sausage", - "Color": "#cf5858", - "Display": 1, - "Order": 2, - "Type": 1 - } - ], - "Code": 1000 -} -` - -var testLabels = []*Label{ - {ID: "LLz8ysmVxwr4dF6mWpClePT0SpSWOEvzTdq17RydSl4ndMckvY1K63HeXDzn03BJQwKYvgf-eWT8Qfd9WVuIEQ==", Name: "CroutonMail is awesome :)", Color: "#7272a7", Order: 1, Display: 0, Type: LabelTypeMailBox}, - {ID: "BvbqbySUPo9uWW_eR8tLA13NUsQMz3P4Zhw4UnpvrKqURnrHlE6L2Au0nplHfHlVXFgGz4L4hJ9-BYllOL-L5g==", Name: "Royal sausage", Color: "#cf5858", Order: 2, Display: 1, Type: LabelTypeMailBox}, -} - -var testLabelReq = LabelReq{&Label{ - Name: "sava", - Color: "#c26cc7", - Display: 1, -}} - -const testCreateLabelBody = `{ - "Label": { - "ID": "otkpEZzG--8dMXvwyLXLQWB72hhBhNGzINjH14rUDfywvOyeN01cDxDrS3Koifxf6asA7Xcwtldm0r_MCmWiAQ==", - "Name": "sava", - "Color": "#c26cc7", - "Display": 1, - "Order": 3, - "Type": 1 - }, - "Code": 1000 -} -` - -var testLabelCreated = &Label{ - ID: "otkpEZzG--8dMXvwyLXLQWB72hhBhNGzINjH14rUDfywvOyeN01cDxDrS3Koifxf6asA7Xcwtldm0r_MCmWiAQ==", - Name: "sava", - Color: "#c26cc7", - Order: 3, - Display: 1, - Type: LabelTypeMailBox, -} - -const testDeleteLabelBody = `{ - "Code": 1000 -} -` - -func TestClient_ListLabels(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/labels?Type=1")) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testLabelsBody) - })) - defer s.Close() - - labels, err := c.ListLabels(context.Background()) - r.NoError(t, err) - r.Equal(t, testLabels, labels) -} - -func TestClient_CreateLabel(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "POST", "/labels")) - - body := &bytes.Buffer{} - _, err := body.ReadFrom(req.Body) - r.NoError(t, err) - - if bytes.Contains(body.Bytes(), []byte("Order")) { - t.Fatal("Body contains `Order`: ", body.String()) - } - - var labelReq LabelReq - err = json.NewDecoder(body).Decode(&labelReq) - r.NoError(t, err) - r.Equal(t, testLabelReq.Label, labelReq.Label) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testCreateLabelBody) - })) - defer s.Close() - - created, err := c.CreateLabel(context.Background(), testLabelReq.Label) - r.NoError(t, err) - - if !reflect.DeepEqual(created, testLabelCreated) { - t.Fatalf("Invalid created label: expected %+v, got %+v", testLabelCreated, created) - } -} - -func TestClient_CreateEmptyLabel(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - r.Fail(t, "API should not be called") - })) - defer s.Close() - - _, err := c.CreateLabel(context.Background(), &Label{}) - r.EqualError(t, err, "name is required") -} - -func TestClient_UpdateLabel(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "PUT", "/labels/"+testLabelCreated.ID)) - - var labelReq LabelReq - err := json.NewDecoder(req.Body).Decode(&labelReq) - r.NoError(t, err) - r.Equal(t, testLabelCreated, labelReq.Label) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testCreateLabelBody) - })) - defer s.Close() - - updated, err := c.UpdateLabel(context.Background(), testLabelCreated) - r.NoError(t, err) - - if !reflect.DeepEqual(updated, testLabelCreated) { - t.Fatalf("Invalid updated label: expected %+v, got %+v", testLabelCreated, updated) - } -} - -func TestClient_UpdateLabelToEmptyName(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - r.Fail(t, "API should not be called") - })) - defer s.Close() - - _, err := c.UpdateLabel(context.Background(), &Label{ID: "label"}) - r.EqualError(t, err, "name is required") -} - -func TestClient_DeleteLabel(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "DELETE", "/labels/"+testLabelCreated.ID)) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testDeleteLabelBody) - })) - defer s.Close() - - err := c.DeleteLabel(context.Background(), testLabelCreated.ID) - r.NoError(t, err) -} - -func TestLeastUsedColor(t *testing.T) { - // No colors at all, should use first available color - colors := []string{} - r.Equal(t, "#7272a7", LeastUsedColor(colors)) - - // All colors have same frequency, should use first available color - colors = []string{"#7272a7", "#cf5858", "#c26cc7", "#7569d1", "#69a9d1", "#5ec7b7", "#72bb75", "#c3d261", "#e6c04c", "#e6984c", "#8989ac", "#cf7e7e", "#c793ca", "#9b94d1", "#a8c4d5", "#97c9c1", "#9db99f", "#c6cd97", "#e7d292", "#dfb286"} - r.Equal(t, "#7272a7", LeastUsedColor(colors)) - - // First three colors already used, but others wasn't. Should use first non-used one. - colors = []string{"#7272a7", "#cf5858", "#c26cc7"} - r.Equal(t, "#7569d1", LeastUsedColor(colors)) -} diff --git a/pkg/pmapi/labels_v4.go b/pkg/pmapi/labels_v4.go deleted file mode 100644 index d337b834..00000000 --- a/pkg/pmapi/labels_v4.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "errors" - "strconv" - - "github.com/go-resty/resty/v2" -) - -type LabelTypeV4 int - -const ( - LabelTypeV4Label = 1 - LabelTypeV4ContactGroup = 2 - LabelTypeV4Folder = 3 -) - -func (c *client) ListLabelsOnly(ctx context.Context) (labels []*Label, err error) { - return c.listLabelTypeV4(ctx, LabelTypeV4Label) -} - -func (c *client) ListFoldersOnly(ctx context.Context) (labels []*Label, err error) { - return c.listLabelTypeV4(ctx, LabelTypeV4Folder) -} - -// listLabelType lists all labels created by the user. -func (c *client) listLabelTypeV4(ctx context.Context, labelType LabelTypeV4) (labels []*Label, err error) { - var res struct { - Labels []*Label - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetQueryParam("Type", strconv.Itoa(int(labelType))).SetResult(&res).Get("/core/v4/labels") - }); err != nil { - return nil, err - } - - return res.Labels, nil -} - -// CreateLabel creates a new label. -func (c *client) CreateLabelV4(ctx context.Context, label *Label) (created *Label, err error) { - if label.Name == "" { - return nil, errors.New("name is required") - } - - var res struct { - Label *Label - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(&LabelReq{ - Label: label, - }).SetResult(&res).Post("/core/v4/labels") - }); err != nil { - return nil, err - } - - return res.Label, nil -} - -// UpdateLabel updates a label. -func (c *client) UpdateLabelV4(ctx context.Context, label *Label) (updated *Label, err error) { - if label.Name == "" { - return nil, errors.New("name is required") - } - - var res struct { - Label *Label - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(&LabelReq{ - Label: label, - }).SetResult(&res).Put("/core/v4/labels/" + label.ID) - }); err != nil { - return nil, err - } - - return res.Label, nil -} - -// DeleteLabel deletes a label. -func (c *client) DeleteLabelV4(ctx context.Context, labelID string) error { - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.Delete("/core/v4/labels/" + labelID) - }); err != nil { - return err - } - - return nil -} diff --git a/pkg/pmapi/labels_v4_test.go b/pkg/pmapi/labels_v4_test.go deleted file mode 100644 index 8cbfcbca..00000000 --- a/pkg/pmapi/labels_v4_test.go +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" - - r "github.com/stretchr/testify/require" -) - -const testFoldersBody = `{ - "Labels": [ - { - "ID": "LLz8ysmVxwr4dF6mWpClePT0SpSWOEvzTdq17RydSl4ndMckvY1K63HeXDzn03BJQwKYvgf-eWT8Qfd9WVuIEQ==", - "Name": "CroutonMail is awesome :)", - "Color": "#7272a7", - "Display": 0, - "Order": 1, - "Type": 3 - }, - { - "ID": "BvbqbySUPo9uWW_eR8tLA13NUsQMz3P4Zhw4UnpvrKqURnrHlE6L2Au0nplHfHlVXFgGz4L4hJ9-BYllOL-L5g==", - "Name": "Royal sausage", - "Color": "#cf5858", - "Display": 1, - "Order": 2, - "Type": 3 - } - ], - "Code": 1000 -} -` - -var testFolders = []*Label{ - {ID: "LLz8ysmVxwr4dF6mWpClePT0SpSWOEvzTdq17RydSl4ndMckvY1K63HeXDzn03BJQwKYvgf-eWT8Qfd9WVuIEQ==", Name: "CroutonMail is awesome :)", Color: "#7272a7", Order: 1, Display: 0, Type: LabelTypeV4Folder}, - {ID: "BvbqbySUPo9uWW_eR8tLA13NUsQMz3P4Zhw4UnpvrKqURnrHlE6L2Au0nplHfHlVXFgGz4L4hJ9-BYllOL-L5g==", Name: "Royal sausage", Color: "#cf5858", Order: 2, Display: 1, Type: LabelTypeV4Folder}, -} - -func TestClient_ListLabelsOnly(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/core/v4/labels?Type=1")) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testLabelsBody) - })) - defer s.Close() - - labels, err := c.ListLabelsOnly(context.Background()) - r.NoError(t, err) - r.Equal(t, testLabels, labels) -} - -func TestClient_ListFoldersOnly(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/core/v4/labels?Type=3")) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testFoldersBody) - })) - defer s.Close() - - folders, err := c.ListFoldersOnly(context.Background()) - r.NoError(t, err) - r.Equal(t, testFolders, folders) -} -func TestClient_CreateLabelV4(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "POST", "/core/v4/labels")) - - body := &bytes.Buffer{} - _, err := body.ReadFrom(req.Body) - r.NoError(t, err) - - if bytes.Contains(body.Bytes(), []byte("Order")) { - t.Fatal("Body contains `Order`: ", body.String()) - } - - var labelReq LabelReq - err = json.NewDecoder(body).Decode(&labelReq) - r.NoError(t, err) - r.Equal(t, testLabelReq.Label, labelReq.Label) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testCreateLabelBody) - })) - defer s.Close() - - created, err := c.CreateLabelV4(context.Background(), testLabelReq.Label) - r.NoError(t, err) - - if !reflect.DeepEqual(created, testLabelCreated) { - t.Fatalf("Invalid created label: expected %+v, got %+v", testLabelCreated, created) - } -} - -func TestClient_CreateEmptyLabelV4(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - r.Fail(t, "API should not be called") - })) - defer s.Close() - - _, err := c.CreateLabelV4(context.Background(), &Label{}) - r.EqualError(t, err, "name is required") -} - -func TestClient_UpdateLabelV4(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "PUT", "/core/v4/labels/"+testLabelCreated.ID)) - - var labelReq LabelReq - err := json.NewDecoder(req.Body).Decode(&labelReq) - r.NoError(t, err) - r.Equal(t, testLabelCreated, labelReq.Label) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testCreateLabelBody) - })) - defer s.Close() - - updated, err := c.UpdateLabelV4(context.Background(), testLabelCreated) - r.NoError(t, err) - - if !reflect.DeepEqual(updated, testLabelCreated) { - t.Fatalf("Invalid updated label: expected %+v, got %+v", testLabelCreated, updated) - } -} - -func TestClient_UpdateLabelToEmptyNameV4(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - r.Fail(t, "API should not be called") - })) - defer s.Close() - - _, err := c.UpdateLabelV4(context.Background(), &Label{ID: "label"}) - r.EqualError(t, err, "name is required") -} - -func TestClient_DeleteLabelV4(t *testing.T) { - s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "DELETE", "/core/v4/labels/"+testLabelCreated.ID)) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testDeleteLabelBody) - })) - defer s.Close() - - err := c.DeleteLabelV4(context.Background(), testLabelCreated.ID) - r.NoError(t, err) -} diff --git a/pkg/pmapi/manager.go b/pkg/pmapi/manager.go deleted file mode 100644 index e0401f05..00000000 --- a/pkg/pmapi/manager.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "net/http" - "sync" - "time" - - "github.com/getsentry/sentry-go" - "github.com/go-resty/resty/v2" -) - -type manager struct { - cfg Config - rc *resty.Client - - isDown bool - locker sync.Locker - refreshingAuth sync.Locker - connectionObservers []ConnectionObserver - proxyDialer *ProxyTLSDialer - - pingMutex *sync.RWMutex - isPinging bool - setSentryUserIDOnce sync.Once -} - -func New(cfg Config) Manager { - return newManager(cfg) -} - -func newManager(cfg Config) *manager { - m := &manager{ - cfg: cfg, - rc: resty.New().EnableTrace(), - locker: &sync.Mutex{}, - refreshingAuth: &sync.Mutex{}, - pingMutex: &sync.RWMutex{}, - isPinging: false, - setSentryUserIDOnce: sync.Once{}, - } - - proxyDialer, transport := newProxyDialerAndTransport(cfg) - m.proxyDialer = proxyDialer - m.rc.SetTransport(transport) - - m.rc.SetBaseURL(cfg.HostURL) - m.rc.OnBeforeRequest(m.setHeaderValues) - - // Any HTTP status code higher than 399 with JSON inside (and proper header) - // is converted to Error. `catchAPIError` then processes API custom errors - // wrapped in JSON. If error is returned, `handleRequestFailure` is called, - // otherwise `handleRequestSuccess` is called. - m.rc.SetError(&Error{}) - m.rc.OnAfterResponse(logConnReuse) - m.rc.OnAfterResponse(updateTime) - m.rc.OnAfterResponse(m.catchAPIError) - m.rc.OnAfterResponse(m.handleRequestSuccess) - m.rc.OnError(m.handleRequestFailure) - - // Configure retry mechanism. - // - // SetRetryCount(5): The most probable value of Retry-After from our - // API is 1s (max 10s). Retrying up to 5 times will on average cause a - // delay of 40s. - // - // NOTE: Increasing to values larger than 10 causing significant delay. - // The resty is increasing the delay between retries up to 1 minute - // (SetRetryMaxWaitTime) so for 10 retries the cumulative delay can be - // up to 5min. - m.rc.SetRetryCount(3) - m.rc.SetRetryMaxWaitTime(time.Minute) - m.rc.SetRetryAfter(catchRetryAfter) - m.rc.AddRetryCondition(m.shouldRetry) - - return m -} - -func (m *manager) SetTransport(transport http.RoundTripper) { - m.rc.SetTransport(transport) - m.proxyDialer = nil -} - -func (m *manager) SetCookieJar(jar http.CookieJar) { - m.rc.SetCookieJar(jar) -} - -func (m *manager) SetRetryCount(count int) { - m.rc.SetRetryCount(count) -} - -func (m *manager) AddConnectionObserver(observer ConnectionObserver) { - m.connectionObservers = append(m.connectionObservers, observer) -} - -func (m *manager) setHeaderValues(_ *resty.Client, req *resty.Request) error { - req.SetHeaders(map[string]string{ - "x-pm-appversion": m.cfg.AppVersion, - "User-Agent": m.cfg.getUserAgent(), - }) - return nil -} - -func (m *manager) r(ctx context.Context) *resty.Request { - return m.rc.R().SetContext(ctx) -} - -func (m *manager) handleRequestSuccess(_ *resty.Client, res *resty.Response) error { - m.locker.Lock() - defer m.locker.Unlock() - - if !m.isDown { - return nil - } - - // We successfully got a response; connection must be up. - - m.isDown = false - - for _, observer := range m.connectionObservers { - observer.OnUp() - } - - return nil -} - -func (m *manager) handleRequestFailure(req *resty.Request, err error) { - m.locker.Lock() - defer m.locker.Unlock() - - if m.isDown { - return - } - - if res, ok := err.(*resty.ResponseError); ok && res.Response.RawResponse != nil { - return - } - - // We didn't get any response; connection must be down. - - m.isDown = true - - for _, observer := range m.connectionObservers { - observer.OnDown() - } - - go m.pingUntilSuccess() -} - -func (m *manager) setSentryUserID(userID string) { - m.setSentryUserIDOnce.Do(func() { - sentry.ConfigureScope(func(scope *sentry.Scope) { - scope.SetTag("UserID", userID) - }) - }) -} diff --git a/pkg/pmapi/manager_auth.go b/pkg/pmapi/manager_auth.go deleted file mode 100644 index 65d53266..00000000 --- a/pkg/pmapi/manager_auth.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "encoding/base64" - "time" - - "github.com/ProtonMail/go-srp" -) - -func (m *manager) NewClient(uid, acc, ref string, exp time.Time) Client { - log.Trace("New client") - - return newClient(m, uid).withAuth(acc, ref, exp) -} - -func (m *manager) NewClientWithRefresh(ctx context.Context, uid, ref string) (Client, *AuthRefresh, error) { - log.Trace("New client with refresh") - - c := newClient(m, uid) - - auth, err := m.authRefresh(ctx, uid, ref) - if err != nil { - return nil, nil, err - } - - return c.withAuth(auth.AccessToken, auth.RefreshToken, expiresIn(auth.ExpiresIn)), auth, nil -} - -func (m *manager) NewClientWithLogin(ctx context.Context, username string, password []byte) (Client, *Auth, error) { - log.Trace("New client with login") - - info, err := m.getAuthInfo(ctx, GetAuthInfoReq{Username: username}) - if err != nil { - return nil, nil, err - } - - srpAuth, err := srp.NewAuth(info.Version, username, password, info.Salt, info.Modulus, info.ServerEphemeral) - if err != nil { - return nil, nil, err - } - - proofs, err := srpAuth.GenerateProofs(2048) - if err != nil { - return nil, nil, err - } - - // Do not retry requests after this point. The ephemeral from auth info - // won't be valid any more - ctx = ContextWithoutRetry(ctx) - - auth, err := m.auth(ctx, AuthReq{ - Username: username, - ClientProof: base64.StdEncoding.EncodeToString(proofs.ClientProof), - ClientEphemeral: base64.StdEncoding.EncodeToString(proofs.ClientEphemeral), - SRPSession: info.SRPSession, - }) - if err != nil { - return nil, nil, err - } - - return newClient(m, auth.UID).withAuth(auth.AccessToken, auth.RefreshToken, expiresIn(auth.ExpiresIn)), auth, nil -} - -func (m *manager) getAuthInfo(ctx context.Context, req GetAuthInfoReq) (*AuthInfo, error) { - var res struct { - *AuthInfo - } - - _, err := wrapNoConnection(m.r(ctx).SetBody(req).SetResult(&res).Post("/auth/info")) - if err != nil { - return nil, err - } - - return res.AuthInfo, nil -} - -func (m *manager) auth(ctx context.Context, req AuthReq) (*Auth, error) { - var res struct { - *Auth - } - - _, err := wrapNoConnection(m.r(ctx).SetBody(req).SetResult(&res).Post("/auth")) - if err != nil { - return nil, err - } - - return res.Auth, nil -} - -func (m *manager) authRefresh(ctx context.Context, uid, ref string) (*AuthRefresh, error) { - m.refreshingAuth.Lock() - defer m.refreshingAuth.Unlock() - - req := authRefreshReq{ - UID: uid, - RefreshToken: ref, - ResponseType: "token", - GrantType: "refresh_token", - RedirectURI: "https://protonmail.ch", - State: randomString(32), - } - - var res struct { - *AuthRefresh - } - - _, err := wrapNoConnection(m.r(ctx).SetBody(req).SetResult(&res).Post("/auth/refresh")) - if err != nil { - if IsBadRequest(err) || IsUnprocessableEntity(err) { - err = ErrAuthFailed{err} - } - return nil, err - } - - return res.AuthRefresh, nil -} - -func expiresIn(seconds int64) time.Time { - return time.Now().Add(time.Duration(seconds) * time.Second) -} diff --git a/pkg/pmapi/manager_download.go b/pkg/pmapi/manager_download.go deleted file mode 100644 index 55fab5b3..00000000 --- a/pkg/pmapi/manager_download.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "io" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "golang.org/x/net/context" -) - -// DownloadAndVerify downloads a file and its signature from the given locations `file` and `sig`. -// The file and its signature are verified using the given keyring `kr`. -// If the file is verified successfully, it can be read from the returned reader. -// TLS fingerprinting is used to verify that connections are only made to known servers. -func (m *manager) DownloadAndVerify(kr *crypto.KeyRing, url, sig string) ([]byte, error) { - fb, err := m.fetchFile(url) - if err != nil { - return nil, err - } - - sb, err := m.fetchFile(sig) - if err != nil { - return nil, err - } - - if err := kr.VerifyDetached( - crypto.NewPlainMessage(fb), - crypto.NewPGPSignature(sb), - crypto.GetUnixTime(), - ); err != nil { - return nil, err - } - - return fb, nil -} - -func (m *manager) fetchFile(url string) ([]byte, error) { - res, err := m.r(ContextWithoutRetry(context.Background())).SetDoNotParseResponse(true).Get(url) - if err != nil { - return nil, err - } - - b, err := io.ReadAll(res.RawBody()) - if err != nil { - return nil, err - } - - if err := res.RawBody().Close(); err != nil { - return nil, err - } - - return b, nil -} diff --git a/pkg/pmapi/manager_log.go b/pkg/pmapi/manager_log.go deleted file mode 100644 index dd329f7a..00000000 --- a/pkg/pmapi/manager_log.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "github.com/go-resty/resty/v2" - "github.com/sirupsen/logrus" -) - -// restyLogger decreases debug level to trace level so resty logs -// are not logged as debug but trace instead. Resty logging is too -// verbose which we don't want to have in debug level. -type restyLogger struct { - logrus *logrus.Entry -} - -func (l *restyLogger) Errorf(format string, v ...interface{}) { - l.logrus.Errorf(format, v...) -} - -func (l *restyLogger) Warnf(format string, v ...interface{}) { - l.logrus.Warnf(format, v...) -} - -func (l *restyLogger) Debugf(format string, v ...interface{}) { - l.logrus.Tracef(format, v...) -} - -func (m *manager) SetLogging(logger *logrus.Entry, verbose bool) { - if verbose { - m.rc.SetLogger(&restyLogger{logrus: logger}) - m.rc.SetDebug(true) - return - } - - m.rc.OnBeforeRequest(func(_ *resty.Client, req *resty.Request) error { - logger.Infof("Requesting %s %s", req.Method, req.URL) - return nil - }) - m.rc.OnAfterResponse(func(_ *resty.Client, res *resty.Response) error { - log := logger.WithFields(logrus.Fields{ - "error": res.Error(), - "status": res.StatusCode(), - "duration": res.Time(), - }) - if res.Request == nil { - log.Warn("Requested unknown request") - return nil - } - log.Debugf("Requested %s %s", res.Request.Method, res.Request.URL) - return nil - }) - m.rc.OnError(func(req *resty.Request, err error) { - logger.WithError(err).Warnf("Failed request %s %s", req.Method, req.URL) - }) -} diff --git a/pkg/pmapi/manager_metrics.go b/pkg/pmapi/manager_metrics.go deleted file mode 100644 index d24ff13a..00000000 --- a/pkg/pmapi/manager_metrics.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" -) - -func (m *manager) SendSimpleMetric(ctx context.Context, category, action, label string) error { - r := m.r(ctx).SetQueryParams(map[string]string{ - "Category": category, - "Action": action, - "Label": label, - }) - if _, err := wrapNoConnection(r.Get("/metrics")); err != nil { - return err - } - return nil -} diff --git a/pkg/pmapi/manager_metrics_test.go b/pkg/pmapi/manager_metrics_test.go deleted file mode 100644 index 1bd9e42c..00000000 --- a/pkg/pmapi/manager_metrics_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - r "github.com/stretchr/testify/require" -) - -const testSendSimpleMetricsBody = `{ - "Code": 1000 -} -` - -func TestClient_SendSimpleMetric(t *testing.T) { - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/metrics?Action=some_action&Category=some_category&Label=some_label")) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, testSendSimpleMetricsBody) - })) - defer s.Close() - - m := newManager(newTestConfig(s.URL)) - - err := m.SendSimpleMetric(context.Background(), "some_category", "some_action", "some_label") - r.NoError(t, err) -} diff --git a/pkg/pmapi/manager_ping.go b/pkg/pmapi/manager_ping.go deleted file mode 100644 index 0835eece..00000000 --- a/pkg/pmapi/manager_ping.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "time" - - "github.com/sirupsen/logrus" -) - -// retryConnectionSleeps defines a smooth cool down in seconds. -var retryConnectionSleeps = []int{2, 5, 10, 30, 60} //nolint:gochecknoglobals - -func (m *manager) pingUntilSuccess() { - if m.isPingOngoing() { - logrus.Debug("Ping already ongoing") - return - } - m.pingingStarted() - defer m.pingingStopped() - - attempt := 0 - for { - ctx := ContextWithoutRetry(context.Background()) - err := m.testPing(ctx) - if err == nil { - return - } - - waitTime := getRetryConnectionSleep(attempt) - attempt++ - logrus.WithError(err).WithField("attempt", attempt).WithField("wait", waitTime).Debug("Connection (still) not available") - time.Sleep(waitTime) - } -} - -func (m *manager) isPingOngoing() bool { - m.pingMutex.RLock() - defer m.pingMutex.RUnlock() - - return m.isPinging -} - -func (m *manager) pingingStarted() { - m.pingMutex.Lock() - defer m.pingMutex.Unlock() - m.isPinging = true -} - -func (m *manager) pingingStopped() { - m.pingMutex.Lock() - defer m.pingMutex.Unlock() - m.isPinging = false -} - -func getRetryConnectionSleep(idx int) time.Duration { - if idx >= len(retryConnectionSleeps) { - idx = len(retryConnectionSleeps) - 1 - } - sec := retryConnectionSleeps[idx] - return time.Duration(sec) * time.Second -} - -func (m *manager) testPing(ctx context.Context) error { - if _, err := m.r(ctx).Get("/tests/ping"); err != nil { - return err - } - return nil -} diff --git a/pkg/pmapi/manager_proxy.go b/pkg/pmapi/manager_proxy.go deleted file mode 100644 index 814b567d..00000000 --- a/pkg/pmapi/manager_proxy.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -// AllowProxy allows the client manager to switch clients over to a proxy if need be. -func (m *manager) AllowProxy() { - if m.proxyDialer != nil { - m.proxyDialer.AllowProxy() - } -} - -// DisallowProxy prevents the client manager from switching clients over to a proxy if need be. -func (m *manager) DisallowProxy() { - if m.proxyDialer != nil { - m.proxyDialer.DisallowProxy() - } -} diff --git a/pkg/pmapi/manager_report.go b/pkg/pmapi/manager_report.go deleted file mode 100644 index 813d75fd..00000000 --- a/pkg/pmapi/manager_report.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" -) - -// Report sends request as json or multipart (if has attachment). -func (m *manager) ReportBug(ctx context.Context, rep ReportBugReq) error { - if rep.ClientType == 0 { - rep.ClientType = EmailClientType - } - - if rep.Client == "" { - rep.Client = m.cfg.GetUserAgent() - } - - if rep.ClientVersion == "" { - rep.ClientVersion = m.cfg.AppVersion - } - - r := m.r(ctx).SetMultipartFormData(rep.GetMultipartFormData()) - - for _, att := range rep.Attachments { - r = r.SetMultipartField(att.name, att.name, att.mime, att.body) - } - - if _, err := wrapNoConnection(r.Post("/reports/bug")); err != nil { - return err - } - - return nil -} diff --git a/pkg/pmapi/manager_report_test.go b/pkg/pmapi/manager_report_test.go deleted file mode 100644 index 65738991..00000000 --- a/pkg/pmapi/manager_report_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - r "github.com/stretchr/testify/require" -) - -var testBugReportReq = ReportBugReq{ - OS: "Mac OSX", - OSVersion: "10.11.6", - Browser: "AppleMail", - Client: "demoapp", - ClientVersion: "GoPMAPI_1.0.14", - ClientType: 1, - Title: "Big Bug", - Description: "Cannot fetch new messages", - Username: "Apple", - Email: "apple@gmail.com", -} - -const testBugsBody = `{ - "Code": 1000 -} -` - -func TestClient_BugReportWithAttachment(t *testing.T) { - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "POST", "/reports/bug")) - r.NoError(t, req.ParseMultipartForm(10*1024)) - - for field, expected := range map[string]string{ - "OS": testBugReportReq.OS, - "OSVersion": testBugReportReq.OSVersion, - "Client": testBugReportReq.Client, - "ClientVersion": testBugReportReq.ClientVersion, - "ClientType": fmt.Sprintf("%d", testBugReportReq.ClientType), - "Title": testBugReportReq.Title, - "Description": testBugReportReq.Description, - "Username": testBugReportReq.Username, - "Email": testBugReportReq.Email, - } { - r.Equal(t, expected, req.PostFormValue(field)) - } - - attReader, err := req.MultipartForm.File["log"][0].Open() - r.NoError(t, err) - _, err = io.ReadAll(attReader) - r.NoError(t, err) - - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testBugsBody) - })) - defer s.Close() - - cm := newManager(newTestConfig(s.URL)) - - rep := testBugReportReq - rep.AddAttachment("log", "last.log", strings.NewReader(testAttachmentJSON)) - - err := cm.ReportBug(context.Background(), rep) - r.NoError(t, err) -} diff --git a/pkg/pmapi/manager_report_types.go b/pkg/pmapi/manager_report_types.go deleted file mode 100644 index 5721d607..00000000 --- a/pkg/pmapi/manager_report_types.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "fmt" - "io" -) - -// ClientType is required by API. -const ( - EmailClientType = iota + 1 - VPNClientType -) - -type reportAtt struct { - name, mime string - body io.Reader -} - -// ReportBugReq stores data for report. -type ReportBugReq struct { - OS string `json:",omitempty"` - OSVersion string `json:",omitempty"` - Browser string `json:",omitempty"` - BrowserVersion string `json:",omitempty"` - BrowserExtensions string `json:",omitempty"` - Resolution string `json:",omitempty"` - DisplayMode string `json:",omitempty"` - Client string `json:",omitempty"` - ClientVersion string `json:",omitempty"` - ClientType int `json:",omitempty"` - Title string `json:",omitempty"` - Description string `json:",omitempty"` - Username string `json:",omitempty"` - Email string `json:",omitempty"` - Country string `json:",omitempty"` - ISP string `json:",omitempty"` - Debug string `json:",omitempty"` - Attachments []reportAtt `json:",omitempty"` -} - -// AddAttachment to report. -func (rep *ReportBugReq) AddAttachment(name, mime string, r io.Reader) { - rep.Attachments = append(rep.Attachments, reportAtt{name: name, mime: mime, body: r}) -} - -func (rep *ReportBugReq) GetMultipartFormData() map[string]string { - return map[string]string{ - "OS": rep.OS, - "OSVersion": rep.OSVersion, - "Browser": rep.Browser, - "BrowserVersion": rep.BrowserVersion, - "BrowserExtensions": rep.BrowserExtensions, - "Resolution": rep.Resolution, - "DisplayMode": rep.DisplayMode, - "Client": rep.Client, - "ClientVersion": rep.ClientVersion, - "ClientType": fmt.Sprintf("%d", rep.ClientType), - "Title": rep.Title, - "Description": rep.Description, - "Username": rep.Username, - "Email": rep.Email, - "Country": rep.Country, - "ISP": rep.ISP, - "Debug": rep.Debug, - } -} diff --git a/pkg/pmapi/manager_test.go b/pkg/pmapi/manager_test.go deleted file mode 100644 index 3454d21e..00000000 --- a/pkg/pmapi/manager_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - r "github.com/stretchr/testify/require" -) - -const testForceUpgradeBody = `{ - "Code":5003, - "Error":"Upgrade!" -}` - -const testTooManyAPIRequests = `{ - "Code":85131, - "Error":"Too many recent API requests" -}` - -func TestHandleTooManyRequests(t *testing.T) { - var numCalls int - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - numCalls++ - - if numCalls < 5 { - w.WriteHeader(http.StatusTooManyRequests) - w.Header().Set("content-type", "application/json;charset=utf-8") - fmt.Fprint(w, testTooManyAPIRequests) - } else { - w.WriteHeader(http.StatusOK) - } - })) - - m := New(Config{HostURL: ts.URL}) - - m.SetRetryCount(5) - - // The call should succeed because the 5th retry should succeed (429s are retried). - _, err := m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(context.Background()) - r.NoError(t, err) - - // The server should be called 5 times. - // The first four calls should return 429 and the last call should return 200. - r.Equal(t, 5, numCalls) -} - -func TestHandleUnprocessableEntity(t *testing.T) { - var numCalls int - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - numCalls++ - w.WriteHeader(http.StatusUnprocessableEntity) - })) - - m := New(Config{HostURL: ts.URL}) - - m.SetRetryCount(5) - - // The call should fail because the first call should fail (422s are not retried). - _, err := m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(context.Background()) - r.EqualError(t, err, "422 Unprocessable Entity") - // The server should be called 1 time. - // The first call should return 422. - r.Equal(t, 1, numCalls) -} - -func TestHandleDialFailure(t *testing.T) { - var numCalls int - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - numCalls++ - w.WriteHeader(http.StatusOK) - })) - - // The failingRoundTripper will fail the first 5 times it is used. - m := New(Config{HostURL: ts.URL}) - m.SetTransport(newFailingRoundTripper(5)) - m.SetRetryCount(5) - - // The call should succeed because the last retry should succeed (dial errors are retried). - _, err := m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(context.Background()) - r.NoError(t, err) - - // The server should be called 1 time. - // The first 4 attempts don't reach the server. - r.Equal(t, 1, numCalls) -} - -func TestHandleTooManyDialFailures(t *testing.T) { - var numCalls int - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - numCalls++ - w.WriteHeader(http.StatusOK) - })) - - // The failingRoundTripper will fail the first 10 times it is used. - // This is more than the number of retries we permit. - // Thus, dials will fail. - m := New(Config{HostURL: ts.URL}) - m.SetTransport(newFailingRoundTripper(10)) - m.SetRetryCount(5) - - // The call should fail because every dial will fail and we'll run out of retries. - _, err := m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(context.Background()) - r.EqualError(t, err, "no internet connection") - // The server should never be called. - r.Equal(t, 0, numCalls) -} - -func TestRetriesWithContextTimeout(t *testing.T) { - var numCalls int - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - numCalls++ - - if numCalls < 5 { - w.WriteHeader(http.StatusTooManyRequests) - } else { - w.WriteHeader(http.StatusOK) - } - })) - - // Theoretically, this should succeed; on the fifth retry, we'll get StatusOK. - m := New(Config{HostURL: ts.URL}) - m.SetRetryCount(5) - - // However, that will take ~0.5s, and we only allow 10ms in the context. - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) - defer cancel() - - // Thus, it will fail. - _, err := m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(ctx) - r.EqualError(t, err, context.DeadlineExceeded.Error()) -} - -func TestObserveConnectionStatus(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - var onDown, onUp bool - - m := New(Config{HostURL: ts.URL}) - m.SetTransport(newFailingRoundTripper(10)) - m.SetRetryCount(5) - m.AddConnectionObserver(NewConnectionObserver(func() { onDown = true }, func() { onUp = true })) - - // The call should fail because every dial will fail and we'll run out of retries. - _, err := m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(context.Background()) - r.Error(t, err) - r.False(t, onUp) - r.True(t, onDown) - - onDown, onUp = false, false - - // The call should succeed because the last dial attempt will succeed. - _, err = m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(context.Background()) - r.NoError(t, err) - r.True(t, onUp) - r.False(t, onDown) -} - -func TestReturnErrNoConnection(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - // We will fail more times than we retry, so requests should fail with ErrNoConnection. - m := New(Config{HostURL: ts.URL}) - m.SetTransport(newFailingRoundTripper(10)) - m.SetRetryCount(5) - - // The call should fail because every dial will fail and we'll run out of retries. - _, err := m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(context.Background()) - r.EqualError(t, err, "no internet connection") -} - -func TestReturnErrUpgradeApplication(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("content-type", "application/json") - w.WriteHeader(http.StatusUnprocessableEntity) - fmt.Fprint(w, testForceUpgradeBody) - })) - - m := New(Config{HostURL: ts.URL}) - - // The call should fail because every call return force upgrade error. - _, err := m.NewClient("", "", "", time.Now().Add(time.Hour)).GetAddresses(context.Background()) - r.EqualError(t, err, ErrUpgradeApplication.Error()) -} - -type failingRoundTripper struct { - http.RoundTripper - - fails, calls int -} - -func newFailingRoundTripper(fails int) http.RoundTripper { - return &failingRoundTripper{ - RoundTripper: http.DefaultTransport, - fails: fails, - } -} - -func (rt *failingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - rt.calls++ - - if rt.calls < rt.fails { - return nil, errors.New("simulating network error") - } - - return rt.RoundTripper.RoundTrip(req) -} diff --git a/pkg/pmapi/manager_types.go b/pkg/pmapi/manager_types.go deleted file mode 100644 index e647e7ed..00000000 --- a/pkg/pmapi/manager_types.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "net/http" - "time" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/sirupsen/logrus" -) - -type Manager interface { - NewClient(string, string, string, time.Time) Client - NewClientWithRefresh(context.Context, string, string) (Client, *AuthRefresh, error) - NewClientWithLogin(context.Context, string, []byte) (Client, *Auth, error) - - DownloadAndVerify(kr *crypto.KeyRing, url, sig string) ([]byte, error) - ReportBug(context.Context, ReportBugReq) error - SendSimpleMetric(context.Context, string, string, string) error - - SetLogging(logger *logrus.Entry, verbose bool) - SetTransport(http.RoundTripper) - SetCookieJar(http.CookieJar) - SetRetryCount(int) - AddConnectionObserver(ConnectionObserver) - - AllowProxy() - DisallowProxy() -} diff --git a/pkg/pmapi/message_send.go b/pkg/pmapi/message_send.go deleted file mode 100644 index 524c2173..00000000 --- a/pkg/pmapi/message_send.go +++ /dev/null @@ -1,363 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "encoding/base64" - "errors" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/go-resty/resty/v2" -) - -// Draft actions. -const ( - DraftActionReply = 0 - DraftActionReplyAll = 1 - DraftActionForward = 2 -) - -// PackageFlag for send message package types. -type PackageFlag int - -func (p *PackageFlag) Has(flag PackageFlag) bool { return iHasFlag(int(*p), int(flag)) } -func (p *PackageFlag) HasAtLeastOne(flag PackageFlag) bool { - return iHasAtLeastOneFlag(int(*p), int(flag)) -} -func (p *PackageFlag) Is(flag PackageFlag) bool { return iIsFlag(int(*p), int(flag)) } -func (p *PackageFlag) HasNo(flag PackageFlag) bool { return iHasNoneOfFlag(int(*p), int(flag)) } - -// Send message package types. -const ( - InternalPackage = PackageFlag(1) - EncryptedOutsidePackage = PackageFlag(2) - ClearPackage = PackageFlag(4) - PGPInlinePackage = PackageFlag(8) - PGPMIMEPackage = PackageFlag(16) - ClearMIMEPackage = PackageFlag(32) -) - -// SignatureFlag for send signature types. -type SignatureFlag int - -func (p *SignatureFlag) Is(flag SignatureFlag) bool { return iIsFlag(int(*p), int(flag)) } -func (p *SignatureFlag) Has(flag SignatureFlag) bool { return iHasFlag(int(*p), int(flag)) } -func (p *SignatureFlag) HasNo(flag SignatureFlag) bool { return iHasNoneOfFlag(int(*p), int(flag)) } - -// Send signature types. -const ( - SignatureNone = SignatureFlag(0) - SignatureDetached = SignatureFlag(1) - SignatureAttachedArmored = SignatureFlag(2) -) - -// DraftReq defines paylod for creating drafts. -type DraftReq struct { - Message *Message - ParentID string `json:",omitempty"` - Action int - AttachmentKeyPackets []string -} - -func (c *client) CreateDraft(ctx context.Context, m *Message, parent string, action int) (created *Message, err error) { - var res struct { - Message *Message - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(&DraftReq{ - Message: m, - ParentID: parent, - Action: action, - AttachmentKeyPackets: []string{}, - }).SetResult(&res).Post("/mail/v4/messages") - }); err != nil { - return nil, err - } - - return res.Message, nil -} - -type AlgoKey struct { - Key string - Algorithm string -} - -type MessageAddress struct { - Type PackageFlag - EncryptedBodyKeyPacket string `json:"BodyKeyPacket,omitempty"` // base64-encoded key packet. - Signature SignatureFlag - EncryptedAttachmentKeyPackets map[string]string `json:"AttachmentKeyPackets,omitempty"` -} - -type MessagePackage struct { - Addresses map[string]*MessageAddress - Type PackageFlag - MIMEType string - EncryptedBody string `json:"Body"` // base64-encoded encrypted data packet. - DecryptedBodyKey *AlgoKey `json:"BodyKey,omitempty"` // base64-encoded session key (only if cleartext recipients). - DecryptedAttachmentKeys map[string]AlgoKey `json:"AttachmentKeys,omitempty"` // Only include if cleartext & attachments. -} - -func newMessagePackage( - send sendData, - attKeys map[string]AlgoKey, -) (pkg *MessagePackage) { - pkg = &MessagePackage{ - EncryptedBody: base64.StdEncoding.EncodeToString(send.ciphertext), - Addresses: send.addressMap, - MIMEType: send.contentType, - Type: send.sharedScheme, - } - - if send.sharedScheme.HasAtLeastOne(ClearPackage | ClearMIMEPackage) { - pkg.DecryptedBodyKey = &AlgoKey{ - Key: send.decryptedBodyKey.GetBase64Key(), - Algorithm: send.decryptedBodyKey.Algo, - } - } - - if len(attKeys) != 0 && send.sharedScheme.Has(ClearPackage) { - pkg.DecryptedAttachmentKeys = attKeys - } - - return pkg -} - -type sendData struct { - decryptedBodyKey *crypto.SessionKey // body session key - addressMap map[string]*MessageAddress - sharedScheme PackageFlag - ciphertext []byte - cleartext string - contentType string -} - -type SendMessageReq struct { - ExpirationTime int64 `json:",omitempty"` - // AutoSaveContacts int `json:",omitempty"` - - // Data for encrypted recipients. - Packages []*MessagePackage `json:",omitempty"` - - mime, plain, rich sendData - attKeys map[string]*crypto.SessionKey - kr *crypto.KeyRing -} - -func NewSendMessageReq( - kr *crypto.KeyRing, - mimeBody, plainBody, richBody string, - attKeys map[string]*crypto.SessionKey, -) *SendMessageReq { - req := &SendMessageReq{} - - req.mime.addressMap = make(map[string]*MessageAddress) - req.plain.addressMap = make(map[string]*MessageAddress) - req.rich.addressMap = make(map[string]*MessageAddress) - - req.mime.cleartext = mimeBody - req.plain.cleartext = plainBody - req.rich.cleartext = richBody - - req.attKeys = attKeys - req.kr = kr - - return req -} - -var ( - errUnknownContentType = errors.New("unknown content type") - errMultipartInNonMIME = errors.New("multipart mixed not allowed in this scheme") - errAttSignNotSupported = errors.New("attached signature not supported") - errEncryptMustSign = errors.New("encrypted package must be signed") - errEncryptedOutsideNotSupported = errors.New("encrypted outside is not supported") - errWrongSendScheme = errors.New("wrong send scheme") - errInternalMustEncrypt = errors.New("internal package must be encrypted") - errInlineMustBePlain = errors.New("PGP Inline package must be plain text") - errMissingPubkey = errors.New("cannot encrypt body key packet: missing pubkey") - errClearSignMustNotBeHTML = errors.New("clear signed packet must be multipart or plain") - errMIMEMustBeMultipart = errors.New("MIME packet must be multipart") - errClearMIMEMustSign = errors.New("clear MIME must be signed") - errClearSignMustNotBePGPInline = errors.New("clear sign must not be PGP inline") -) - -func (req *SendMessageReq) AddRecipient( - email string, sendScheme PackageFlag, - pubkey *crypto.KeyRing, signature SignatureFlag, - contentType string, doEncrypt bool, -) (err error) { - if signature.Has(SignatureAttachedArmored) { - return errAttSignNotSupported - } - - if doEncrypt && signature.HasNo(SignatureDetached) { - return errEncryptMustSign - } - - switch sendScheme { - case PGPMIMEPackage, ClearMIMEPackage: - if contentType != ContentTypeMultipartMixed { - return errMIMEMustBeMultipart - } - return req.addMIMERecipient(email, sendScheme, pubkey, signature) - case InternalPackage, ClearPackage, PGPInlinePackage: - if contentType == ContentTypeMultipartMixed { - return errMultipartInNonMIME - } - return req.addNonMIMERecipient(email, sendScheme, pubkey, signature, contentType, doEncrypt) - case EncryptedOutsidePackage: - return errEncryptedOutsideNotSupported - default: - return errWrongSendScheme - } -} - -func (req *SendMessageReq) addNonMIMERecipient( - email string, sendScheme PackageFlag, - pubkey *crypto.KeyRing, signature SignatureFlag, - contentType string, doEncrypt bool, -) (err error) { - if signature.Is(SignatureDetached) && !doEncrypt { - if sendScheme.Is(PGPInlinePackage) { - return errClearSignMustNotBePGPInline - } - if sendScheme.Is(ClearPackage) && contentType == ContentTypeHTML { - return errClearSignMustNotBeHTML - } - } - - var send *sendData - - switch contentType { - case ContentTypePlainText: - send = &req.plain - send.contentType = ContentTypePlainText - case ContentTypeHTML, "": - send = &req.rich - send.contentType = ContentTypeHTML - case ContentTypeMultipartMixed: - return errMultipartInNonMIME - default: - return errUnknownContentType - } - - if send.decryptedBodyKey == nil { - if send.decryptedBodyKey, send.ciphertext, err = encryptSymmDecryptKey(req.kr, send.cleartext); err != nil { - return err - } - } - newAddress := &MessageAddress{Type: sendScheme, Signature: signature} - - if sendScheme.Is(PGPInlinePackage) && contentType == ContentTypeHTML { - return errInlineMustBePlain - } - if sendScheme.Is(InternalPackage) && !doEncrypt { - return errInternalMustEncrypt - } - if doEncrypt && pubkey == nil { - return errMissingPubkey - } - - if doEncrypt { - newAddress.EncryptedBodyKeyPacket, newAddress.EncryptedAttachmentKeyPackets, err = encryptAndEncodeSessionKeys(pubkey, send.decryptedBodyKey, req.attKeys) - if err != nil { - return err - } - } - send.addressMap[email] = newAddress - send.sharedScheme |= sendScheme - - return nil -} - -func (req *SendMessageReq) addMIMERecipient( - email string, sendScheme PackageFlag, - pubkey *crypto.KeyRing, signature SignatureFlag, -) (err error) { - if sendScheme.Is(ClearMIMEPackage) && signature.HasNo(SignatureDetached) { - return errClearMIMEMustSign - } - - req.mime.contentType = ContentTypeMultipartMixed - if req.mime.decryptedBodyKey == nil { - if req.mime.decryptedBodyKey, req.mime.ciphertext, err = encryptSymmDecryptKey(req.kr, req.mime.cleartext); err != nil { - return err - } - } - - if sendScheme.Is(PGPMIMEPackage) { - if pubkey == nil { - return errMissingPubkey - } - // Attachment keys are not needed because attachments are part - // of MIME body and therefore attachments are encrypted with - // body session key. - mimeBodyPacket, _, err := encryptAndEncodeSessionKeys(pubkey, req.mime.decryptedBodyKey, map[string]*crypto.SessionKey{}) - if err != nil { - return err - } - req.mime.addressMap[email] = &MessageAddress{Type: sendScheme, EncryptedBodyKeyPacket: mimeBodyPacket, Signature: signature} - } else { - req.mime.addressMap[email] = &MessageAddress{Type: sendScheme, Signature: signature} - } - req.mime.sharedScheme |= sendScheme - - return nil -} - -func (req *SendMessageReq) PreparePackages() { - attkeysEncoded := make(map[string]AlgoKey) - for attID, attkey := range req.attKeys { - attkeysEncoded[attID] = AlgoKey{ - Key: attkey.GetBase64Key(), - Algorithm: attkey.Algo, - } - } - - for _, send := range []sendData{req.mime, req.plain, req.rich} { - if len(send.addressMap) == 0 { - continue - } - req.Packages = append(req.Packages, newMessagePackage(send, attkeysEncoded)) - } -} - -func (c *client) SendMessage(ctx context.Context, draftID string, req *SendMessageReq) (*Message, *Message, error) { - if draftID == "" { - return nil, nil, errors.New("pmapi: cannot send message with an empty draftID") - } - - if req.Packages == nil { - req.Packages = []*MessagePackage{} - } - - var res struct { - Sent *Message - Parent *Message - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(req).SetResult(&res).Post("/mail/v4/messages/" + draftID) - }); err != nil { - return nil, nil, err - } - - return res.Sent, res.Parent, nil -} diff --git a/pkg/pmapi/message_send_test.go b/pkg/pmapi/message_send_test.go deleted file mode 100644 index 5f32582f..00000000 --- a/pkg/pmapi/message_send_test.go +++ /dev/null @@ -1,632 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "encoding/base64" - "encoding/json" - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/require" -) - -type recipient struct { - email string - sendScheme PackageFlag - pubkey *crypto.KeyRing - signature SignatureFlag - contentType string - doEncrypt bool - wantError error -} - -type testData struct { - emails []string - recipients []recipient - wantPackages []*MessagePackage - - allRecipients map[string]recipient - allAddresses map[string]*MessageAddress - - attKeys map[string]*crypto.SessionKey - mimeBody, plainBody, richBody string -} - -func (td *testData) addRecipients(t testing.TB) { - for _, email := range td.emails { - rcp, ok := td.allRecipients[email] - require.True(t, ok, "missing recipient %s", email) - rcp.email = email - td.recipients = append(td.recipients, rcp) - } -} - -func (td *testData) addAddresses(t testing.TB) { - for i, wantPackage := range td.wantPackages { - for email := range wantPackage.Addresses { - address, ok := td.allAddresses[email] - require.True(t, ok, "missing address %s", email) - td.wantPackages[i].Addresses[email] = address - } - } -} - -func (td *testData) prepareAndCheck(t *testing.T) { - r := require.New(t) - - matchPresence := func(want string) require.ValueAssertionFunc { - if len(want) == 0 { - return require.Empty - } - return require.NotEmpty - } - - have := NewSendMessageReq(testPrivateKeyRing, td.mimeBody, td.plainBody, td.richBody, td.attKeys) - for _, rec := range td.recipients { - err := have.AddRecipient(rec.email, rec.sendScheme, rec.pubkey, rec.signature, rec.contentType, rec.doEncrypt) - - if rec.wantError == nil { - r.NoError(err, "email %s", rec.email) - } else { - r.EqualError(err, rec.wantError.Error(), "email %s", rec.email) - } - } - have.PreparePackages() - - r.Equal(len(td.wantPackages), len(have.Packages)) - - for i, wantPackage := range td.wantPackages { - havePackage := have.Packages[i] - - r.Equal(wantPackage.MIMEType, havePackage.MIMEType, "pkg %d", i) - r.Equal(wantPackage.Type, havePackage.Type, "pkg %d", i) - - r.Equal(len(wantPackage.Addresses), len(havePackage.Addresses), "pkg %d", i) - for email, wantAddress := range wantPackage.Addresses { - haveAddress, ok := havePackage.Addresses[email] - r.True(ok, "pkg %d email %s", i, email) - - r.Equal(wantAddress.Type, haveAddress.Type, "pkg %d email %s", i, email) - matchPresence(wantAddress.EncryptedBodyKeyPacket)(t, haveAddress.EncryptedBodyKeyPacket, "pkg %d email %s", i, email) - r.Equal(wantAddress.Signature, haveAddress.Signature, "pkg %d email %s", i, email) - - if len(td.attKeys) == 0 { - r.Len(haveAddress.EncryptedAttachmentKeyPackets, 0) - } else { - r.Equal( - len(wantAddress.EncryptedAttachmentKeyPackets), - len(haveAddress.EncryptedAttachmentKeyPackets), - "pkg %d email %s", i, email, - ) - for attID, wantAttKey := range wantAddress.EncryptedAttachmentKeyPackets { - haveAttKey, ok := haveAddress.EncryptedAttachmentKeyPackets[attID] - r.True(ok, "pkg %d email %s att %s", i, email, attID) - matchPresence(wantAttKey)(t, haveAttKey, "pkg %d email %s att %s", i, email, attID) - } - } - } - - matchPresence(wantPackage.EncryptedBody)(t, havePackage.EncryptedBody, "pkg %d", i) - - wantBodyKey := wantPackage.DecryptedBodyKey - haveBodyKey := havePackage.DecryptedBodyKey - - if wantBodyKey == nil { - r.Nil(haveBodyKey, "pkg %d: expected empty body key but got %v", i, haveBodyKey) - } else { - r.NotNil(haveBodyKey, "pkg %d: expected body key but got nil", i) - r.NotEmpty(haveBodyKey.Algorithm, "pkg %d", i) - r.NotEmpty(haveBodyKey.Key, "pkg %d", i) - } - - if len(td.attKeys) == 0 { - r.Len(havePackage.DecryptedAttachmentKeys, 0) - } else { - r.Equal( - len(wantPackage.DecryptedAttachmentKeys), - len(havePackage.DecryptedAttachmentKeys), - "pkg %d", i, - ) - for attID, wantAttKey := range wantPackage.DecryptedAttachmentKeys { - haveAttKey, ok := havePackage.DecryptedAttachmentKeys[attID] - r.True(ok, "pkg %d att %s", i, attID) - matchPresence(wantAttKey.Key)(t, haveAttKey.Key, "pkg %d att %s", i, attID) - matchPresence(wantAttKey.Algorithm)(t, haveAttKey.Algorithm, "pkg %d att %s", i, attID) - } - } - } - - haveBytes, err := json.Marshal(have) - r.NoError(err) - haveString := string(haveBytes) - // Added `:` to avoid false-fail if the whole output results to empty object. - r.NotContains(haveString, ":\"\"", "found empty string: %s", haveString) - r.NotContains(haveString, ":[]", "found empty list: %s", haveString) - r.NotContains(haveString, ":{}", "found empty object: %s", haveString) - r.NotContains(haveString, ":null", "found null: %s", haveString) -} - -func TestSendReq(t *testing.T) { - attKeyB64 := "EvjO/2RIJNn6HdoU6ACqFdZglzJhpjQ/PpjsvL3mB5Q=" - token, err := base64.StdEncoding.DecodeString(attKeyB64) - require.NoError(t, err) - - attKey := crypto.NewSessionKeyFromToken(token, "aes256") - attKeyPackets := map[string]string{"attID": "not-empty"} - attAlgoKeys := map[string]AlgoKey{"attID": {"not-empty", "not-empty"}} - - allRecipients := map[string]recipient{ - // Internal OK - "none@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, "", true, nil}, - "html@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, - "plain@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, - // Internal bad - "wrongtype@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, "application/rfc822", true, errUnknownContentType}, - "multipart@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, errMultipartInNonMIME}, - "noencrypt@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, false, errInternalMustEncrypt}, - "no-pubkey@pm.me": {"", InternalPackage, nil, SignatureDetached, ContentTypeHTML, true, errMissingPubkey}, - "nosigning@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureNone, ContentTypeHTML, true, errEncryptMustSign}, - // testing combination - "internal1@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, - // Clear OK - "html@email.com": {"", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil}, - "none@email.com": {"", ClearPackage, nil, SignatureNone, "", false, nil}, - "plain@email.com": {"", ClearPackage, nil, SignatureNone, ContentTypePlainText, false, nil}, - "plain-sign@email.com": {"", ClearPackage, nil, SignatureDetached, ContentTypePlainText, false, nil}, - "mime-sign@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, false, nil}, - // Clear bad - "mime@email.com": {"", ClearMIMEPackage, nil, SignatureNone, ContentTypeMultipartMixed, false, errClearMIMEMustSign}, - "clear-plain-sign@email.com": {"", PGPInlinePackage, nil, SignatureDetached, ContentTypePlainText, false, errClearSignMustNotBePGPInline}, - "html-sign@email.com": {"", ClearPackage, nil, SignatureDetached, ContentTypeHTML, false, errClearSignMustNotBeHTML}, - "mime-plain@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypePlainText, false, errMIMEMustBeMultipart}, - "mime-html@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypeHTML, false, errMIMEMustBeMultipart}, - // External Encryption OK - "mime@gpg.com": {"", PGPMIMEPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, nil}, - "plain@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, - // External Encryption bad - "eo@gpg.com": {"", EncryptedOutsidePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, errEncryptedOutsideNotSupported}, - "inline-html@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, errInlineMustBePlain}, - "inline-mixed@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, errMultipartInNonMIME}, - "mime-plain@gpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypePlainText, true, errMIMEMustBeMultipart}, - "mime-html@sgpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypeHTML, true, errMIMEMustBeMultipart}, - "no-pubkey@gpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, true, errMissingPubkey}, - "not-signed@gpg.com": {"", PGPMIMEPackage, testPublicKeyRing, SignatureNone, ContentTypeMultipartMixed, true, errEncryptMustSign}, - } - - allAddresses := map[string]*MessageAddress{ - "none@pm.me": { - Type: InternalPackage, - Signature: SignatureDetached, - EncryptedBodyKeyPacket: "not-empty", - EncryptedAttachmentKeyPackets: attKeyPackets, - }, - "plain@pm.me": { - Type: InternalPackage, - Signature: SignatureDetached, - EncryptedBodyKeyPacket: "not-empty", - EncryptedAttachmentKeyPackets: attKeyPackets, - }, - "html@pm.me": { - Type: InternalPackage, - Signature: SignatureDetached, - EncryptedBodyKeyPacket: "not-empty", - EncryptedAttachmentKeyPackets: attKeyPackets, - }, - "internal1@pm.me": { - Type: InternalPackage, - Signature: SignatureDetached, - EncryptedBodyKeyPacket: "not-empty", - EncryptedAttachmentKeyPackets: attKeyPackets, - }, - - "html@email.com": { - Type: ClearPackage, - Signature: SignatureNone, - }, - "none@email.com": { - Type: ClearPackage, - Signature: SignatureNone, - }, - "plain@email.com": { - Type: ClearPackage, - Signature: SignatureNone, - }, - "plain-sign@email.com": { - Type: ClearPackage, - Signature: SignatureDetached, - }, - "mime-sign@email.com": { - Type: ClearMIMEPackage, - Signature: SignatureDetached, - }, - - "mime@gpg.com": { - Type: PGPMIMEPackage, - Signature: SignatureDetached, - EncryptedBodyKeyPacket: "non-empty", - }, - "plain@gpg.com": { - Type: PGPInlinePackage, - Signature: SignatureDetached, - EncryptedBodyKeyPacket: "non-empty", - EncryptedAttachmentKeyPackets: attKeyPackets, - }, - } - - // NOTE naming - // Single: there should be one package - // Multiple: there should be more than one package - // Internal: there should be internal package - // Clear: there should be non-encrypted package - // Encrypted: there should be encrypted package - // NotAllowed: combination of inputs which are not allowed - newTests := map[string]testData{ - "Nothing": { // expect no crash - emails: []string{}, - wantPackages: []*MessagePackage{}, - }, - "Fails": { - emails: []string{ - "wrongtype@pm.me", - "multipart@pm.me", - "noencrypt@pm.me", - "no-pubkey@pm.me", - "nosigning@pm.me", - - "html-sign@email.com", - "mime-plain@email.com", - "mime-html@email.com", - "mime@email.com", - "clear-plain-sign@email.com", - - "eo@gpg.com", - "inline-html@gpg.com", - "inline-mixed@gpg.com", - "mime-plain@gpg.com", - "mime-html@sgpg.com", - "no-pubkey@gpg.com", - "not-signed@gpg.com", - }, - }, - - // one scheme in one package - "SingleInternalHTML": { - emails: []string{"none@pm.me", "html@pm.me"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "none@pm.me": nil, - "html@pm.me": nil, - }, - Type: InternalPackage, - MIMEType: ContentTypeHTML, - EncryptedBody: "non-empty", - }, - }, - }, - "SingleInternalPlain": { - emails: []string{"plain@pm.me"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "plain@pm.me": nil, - }, - Type: InternalPackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - }, - }, - }, - - "SingleClearHTML": { - emails: []string{"none@email.com", "html@email.com"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "html@email.com": nil, - "none@email.com": nil, - }, - Type: ClearPackage, - MIMEType: ContentTypeHTML, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - DecryptedAttachmentKeys: attAlgoKeys, - }, - }, - }, - "SingleClearPlain": { - emails: []string{"plain@email.com", "plain-sign@email.com"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "plain@email.com": nil, - "plain-sign@email.com": nil, - }, - Type: ClearPackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - DecryptedAttachmentKeys: attAlgoKeys, - }, - }, - }, - "SingleClearMIME": { - emails: []string{"mime-sign@email.com"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "mime-sign@email.com": nil, - }, - Type: ClearMIMEPackage, - MIMEType: ContentTypeMultipartMixed, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - }, - }, - }, - - "SingleEncyptedPlain": { - emails: []string{"plain@gpg.com"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "plain@gpg.com": nil, - }, - Type: PGPInlinePackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - }, - }, - }, - "SingleEncyptedMIME": { - emails: []string{"mime@gpg.com"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "mime@gpg.com": nil, - }, - Type: PGPMIMEPackage, - MIMEType: ContentTypeMultipartMixed, - EncryptedBody: "non-empty", - }, - }, - }, - - // two schemes combined to one package - "SingleClearInternalPlain": { - emails: []string{"plain@email.com", "plain-sign@email.com", "plain@pm.me"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "plain@pm.me": nil, - "plain@email.com": nil, - "plain-sign@email.com": nil, - }, - Type: InternalPackage | ClearPackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - DecryptedAttachmentKeys: attAlgoKeys, - }, - }, - }, - "SingleClearInternalHTML": { - emails: []string{"none@email.com", "html@email.com", "html@pm.me", "none@pm.me"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "none@pm.me": nil, - "html@pm.me": nil, - "html@email.com": nil, - "none@email.com": nil, - }, - Type: InternalPackage | ClearPackage, - MIMEType: ContentTypeHTML, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - DecryptedAttachmentKeys: attAlgoKeys, - }, - }, - }, - "SingleEncryptedInternalPlain": { - emails: []string{"plain@gpg.com", "plain@pm.me"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "plain@pm.me": nil, - "plain@gpg.com": nil, - }, - Type: InternalPackage | PGPInlinePackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - }, - }, - }, - "SingleEncryptedClearMIME": { - emails: []string{"mime@gpg.com", "mime-sign@email.com"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "mime@gpg.com": nil, - "mime-sign@email.com": nil, - }, - Type: ClearMIMEPackage | PGPMIMEPackage, - MIMEType: ContentTypeMultipartMixed, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - }, - }, - }, - - // one scheme separated to multiple packages - "MultipleInternal": { - emails: []string{"none@pm.me", "html@pm.me", "plain@pm.me"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "plain@pm.me": nil, - }, - Type: InternalPackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - }, - { - Addresses: map[string]*MessageAddress{ - "none@pm.me": nil, - "html@pm.me": nil, - }, - Type: InternalPackage, - MIMEType: ContentTypeHTML, - EncryptedBody: "non-empty", - }, - }, - }, - "MultipleClear": { - emails: []string{ - "none@email.com", "html@email.com", - "plain@email.com", "plain-sign@email.com", - "mime-sign@email.com", - }, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "mime-sign@email.com": nil, - }, - Type: ClearMIMEPackage, - MIMEType: ContentTypeMultipartMixed, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - }, - { - Addresses: map[string]*MessageAddress{ - "plain@email.com": nil, - "plain-sign@email.com": nil, - }, - Type: ClearPackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - DecryptedAttachmentKeys: attAlgoKeys, - }, - { - Addresses: map[string]*MessageAddress{ - "html@email.com": nil, - "none@email.com": nil, - }, - Type: ClearPackage, - MIMEType: ContentTypeHTML, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - DecryptedAttachmentKeys: attAlgoKeys, - }, - }, - }, - "MultipleEncrypted": { - emails: []string{"plain@gpg.com", "mime@gpg.com"}, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "mime@gpg.com": nil, - }, - Type: PGPMIMEPackage, - MIMEType: ContentTypeMultipartMixed, - EncryptedBody: "non-empty", - }, - { - Addresses: map[string]*MessageAddress{ - "plain@gpg.com": nil, - }, - Type: PGPInlinePackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - }, - }, - }, - - "MultipleComboAll": { - emails: []string{ - "none@pm.me", - "plain@pm.me", - "html@pm.me", - - "none@email.com", - "html@email.com", - "plain@email.com", - "plain-sign@email.com", - "mime-sign@email.com", - - "mime@gpg.com", - "plain@gpg.com", - }, - wantPackages: []*MessagePackage{ - { - Addresses: map[string]*MessageAddress{ - "mime@gpg.com": nil, - "mime-sign@email.com": nil, - }, - Type: ClearMIMEPackage | PGPMIMEPackage, - MIMEType: ContentTypeMultipartMixed, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - }, - { - Addresses: map[string]*MessageAddress{ - "plain@gpg.com": nil, - "plain@email.com": nil, - "plain-sign@email.com": nil, - "plain@pm.me": nil, - }, - Type: InternalPackage | ClearPackage | PGPInlinePackage, - MIMEType: ContentTypePlainText, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - DecryptedAttachmentKeys: attAlgoKeys, - }, - { - Addresses: map[string]*MessageAddress{ - "none@pm.me": nil, - "html@pm.me": nil, - "none@email.com": nil, - "html@email.com": nil, - }, - Type: InternalPackage | ClearPackage, - MIMEType: ContentTypeHTML, - EncryptedBody: "non-empty", - DecryptedBodyKey: &AlgoKey{"non-empty", "non-empty"}, - DecryptedAttachmentKeys: attAlgoKeys, - }, - }, - }, - } - - for name, test := range newTests { - test.mimeBody = "Mime body" - test.plainBody = "Plain body" - test.richBody = "HTML body" - test.allRecipients = allRecipients - test.allAddresses = allAddresses - - test.addRecipients(t) - test.addAddresses(t) - - t.Run("NoAtt"+name, test.prepareAndCheck) - test.attKeys = map[string]*crypto.SessionKey{"attID": attKey} - t.Run("Att"+name, test.prepareAndCheck) - } -} diff --git a/pkg/pmapi/messages.go b/pkg/pmapi/messages.go deleted file mode 100644 index 5cb4df83..00000000 --- a/pkg/pmapi/messages.go +++ /dev/null @@ -1,739 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "bytes" - "context" - "crypto/aes" - "crypto/cipher" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/mail" - "net/url" - "regexp" - "strconv" - "strings" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/go-resty/resty/v2" - "github.com/sirupsen/logrus" -) - -// Header types. -const ( - MessageHeader = "-----BEGIN PGP MESSAGE-----" - MessageTail = "-----END PGP MESSAGE-----" - MessageHeaderLegacy = "---BEGIN ENCRYPTED MESSAGE---" - MessageTailLegacy = "---END ENCRYPTED MESSAGE---" - RandomKeyHeader = "---BEGIN ENCRYPTED RANDOM KEY---" - RandomKeyTail = "---END ENCRYPTED RANDOM KEY---" -) - -// Sort types. -const ( - SortByTo = "To" - SortByFrom = "From" - SortBySubject = "Subject" - SortBySize = "Size" - SortByTime = "Time" - SortByID = "ID" - SortDesc = true - SortAsc = false -) - -// Message actions. -const ( - ActionReply = 0 - ActionReplyAll = 1 - ActionForward = 2 -) - -// Message flag definitions. -const ( - FlagReceived = int64(1) - FlagSent = int64(2) - FlagInternal = int64(4) - FlagE2E = int64(8) - FlagAuto = int64(16) - FlagReplied = int64(32) - FlagRepliedAll = int64(64) - FlagForwarded = int64(128) - - FlagAutoreplied = int64(256) - FlagImported = int64(512) - FlagOpened = int64(1024) - FlagReceiptSent = int64(2048) -) - -// Draft flags. -const ( - FlagReceiptRequest = 1 << 16 - FlagPublicKey = 1 << 17 - FlagSign = 1 << 18 -) - -// Spam flags. -const ( - FlagSpfFail = 1 << 24 - FlagDkimFail = 1 << 25 - FlagDmarcFail = 1 << 26 - FlagHamManual = 1 << 27 - FlagSpamAuto = 1 << 28 - FlagSpamManual = 1 << 29 - FlagPhishingAuto = 1 << 30 - FlagPhishingManual = 1 << 31 -) - -// Message flag masks. -const ( - FlagMaskGeneral = 4095 - FlagMaskDraft = FlagReceiptRequest * 7 - FlagMaskSpam = FlagSpfFail * 255 - FlagMask = FlagMaskGeneral | FlagMaskDraft | FlagMaskSpam -) - -// INTERNAL, AUTO are immutable. E2E is immutable except for drafts on send. -const ( - FlagMaskAdd = 4067 + (16777216 * 168) -) - -// Content types. -const ( - ContentTypeMultipartMixed = "multipart/mixed" - ContentTypeMultipartEncrypted = "multipart/encrypted" - ContentTypePlainText = "text/plain" - ContentTypeHTML = "text/html" -) - -// LabelsOperation is the operation to apply to labels. -type LabelsOperation int - -const ( - KeepLabels LabelsOperation = iota // KeepLabels Do nothing. - ReplaceLabels // ReplaceLabels Replace current labels with new ones. - AddLabels // AddLabels Add new labels to current ones. - RemoveLabels // RemoveLabels Remove specified labels from current ones. -) - -// Due to API limitations, we shouldn't make requests with more than 100 message IDs at a time. -const messageIDPageSize = 100 - -// ConversationIDDomain is used as a placeholder for conversation reference headers to improve compatibility with various clients. -const ConversationIDDomain = `protonmail.conversationid` - -// InternalIDDomain is used as a placeholder for reference/message ID headers to improve compatibility with various clients. -const InternalIDDomain = `protonmail.internalid` - -// RxInternalReferenceFormat is compiled regexp which describes the match for -// a message ID used in reference headers. -var RxInternalReferenceFormat = regexp.MustCompile(`(?U)<(.+)@` + regexp.QuoteMeta(InternalIDDomain) + `>`) //nolint:gochecknoglobals - -// Message structure. -type Message struct { - ID string `json:",omitempty"` - Order int64 `json:",omitempty"` - ConversationID string `json:",omitempty"` // only filter - Subject string - Unread Boolean - Flags int64 - Sender *mail.Address - ReplyTo *mail.Address `json:",omitempty"` - ReplyTos []*mail.Address `json:",omitempty"` - ToList []*mail.Address - CCList []*mail.Address - BCCList []*mail.Address - Time int64 // Unix time - NumAttachments int - ExpirationTime int64 // Unix time - SpamScore int - AddressID string - Body string `json:",omitempty"` - Attachments []*Attachment - LabelIDs []string - ExternalID string - Header mail.Header - MIMEType string -} - -// NewMessage initializes a new message. -func NewMessage() *Message { - return &Message{ - ToList: []*mail.Address{}, - CCList: []*mail.Address{}, - BCCList: []*mail.Address{}, - Attachments: []*Attachment{}, - LabelIDs: []string{}, - } -} - -// Define a new type to prevent MarshalJSON/UnmarshalJSON infinite loops. -type message Message - -type rawMessage struct { - message - - Header string `json:",omitempty"` -} - -func (m *Message) MarshalJSON() ([]byte, error) { - var raw rawMessage - raw.message = message(*m) - - b := &bytes.Buffer{} - _ = http.Header(m.Header).Write(b) - raw.Header = b.String() - - return json.Marshal(&raw) -} - -func (m *Message) UnmarshalJSON(b []byte) error { - var raw rawMessage - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - - *m = Message(raw.message) - - if raw.Header != "" && raw.Header != "(No Header)" { - msg, err := mail.ReadMessage(strings.NewReader(raw.Header + "\r\n\r\n")) - if err != nil { - logrus.WithField("rawHeader", raw.Header).Trace("Failed to parse header") - return fmt.Errorf("failed to parse header of message %v: %v", m.ID, err.Error()) - } - m.Header = msg.Header - } else { - m.Header = make(mail.Header) - } - - return nil -} - -// IsDraft returns whether the message should be considered to be a draft. -// A draft is complicated. It might have pmapi.DraftLabel but it might not. -// The real API definition of IsDraft is that it is neither sent nor received -- we should use that here. -func (m *Message) IsDraft() bool { - return (m.Flags & (FlagReceived | FlagSent)) == 0 -} - -// HasLabelID returns whether the message has the `labelID`. -func (m *Message) HasLabelID(labelID string) bool { - for _, l := range m.LabelIDs { - if l == labelID { - return true - } - } - return false -} - -func (m *Message) IsEncrypted() bool { - return strings.HasPrefix(m.Header.Get("Content-Type"), "multipart/encrypted") || m.IsBodyEncrypted() -} - -func (m *Message) IsBodyEncrypted() bool { - trimmedBody := strings.TrimSpace(m.Body) - return strings.HasPrefix(trimmedBody, MessageHeader) && - strings.HasSuffix(trimmedBody, MessageTail) -} - -func (m *Message) IsLegacyMessage() bool { - return strings.Contains(m.Body, RandomKeyHeader) && - strings.Contains(m.Body, RandomKeyTail) && - strings.Contains(m.Body, MessageHeaderLegacy) && - strings.Contains(m.Body, MessageTailLegacy) && - strings.Contains(m.Body, MessageHeader) && - strings.Contains(m.Body, MessageTail) -} - -func (m *Message) Decrypt(kr *crypto.KeyRing) ([]byte, error) { - if m.IsLegacyMessage() { - return m.decryptLegacy(kr) - } - - if !m.IsBodyEncrypted() { - return []byte(m.Body), nil - } - - armored := strings.TrimSpace(m.Body) - - body, err := decrypt(kr, armored) - if err != nil { - return nil, err - } - - return body, nil -} - -type Signature struct { - Hash string - Data []byte -} - -func (m *Message) ExtractSignatures(kr *crypto.KeyRing) ([]Signature, error) { - var entities openpgp.EntityList - - for _, key := range kr.GetKeys() { - entities = append(entities, key.GetEntity()) - } - - p, err := armor.Decode(strings.NewReader(m.Body)) - if err != nil { - return nil, err - } - - msg, err := openpgp.ReadMessage(p.Body, entities, nil, nil) - if err != nil { - return nil, err - } - - if _, err := io.ReadAll(msg.UnverifiedBody); err != nil { - return nil, err - } - - if !msg.IsSigned { - return nil, nil - } - - signatures := make([]Signature, 0, len(msg.UnverifiedSignatures)) - - for _, signature := range msg.UnverifiedSignatures { - buf := new(bytes.Buffer) - - if err := signature.Serialize(buf); err != nil { - return nil, err - } - - signatures = append(signatures, Signature{ - Hash: signature.Hash.String(), - Data: buf.Bytes(), - }) - } - - return signatures, nil -} - -func (m *Message) decryptLegacy(kr *crypto.KeyRing) (dec []byte, err error) { - randomKeyStart := strings.Index(m.Body, RandomKeyHeader) + len(RandomKeyHeader) - randomKeyEnd := strings.Index(m.Body, RandomKeyTail) - randomKey := m.Body[randomKeyStart:randomKeyEnd] - - signedKey, err := decrypt(kr, strings.TrimSpace(randomKey)) - if err != nil { - return - } - bytesKey, err := decodeBase64UTF8(string(signedKey)) - if err != nil { - return - } - - messageStart := strings.Index(m.Body, MessageHeaderLegacy) + len(MessageHeaderLegacy) - messageEnd := strings.Index(m.Body, MessageTailLegacy) - message := m.Body[messageStart:messageEnd] - bytesMessage, err := decodeBase64UTF8(message) - if err != nil { - return - } - - block, err := aes.NewCipher(bytesKey) - if err != nil { - return - } - - prefix := make([]byte, block.BlockSize()+2) - bytesMessageReader := bytes.NewReader(bytesMessage) - - _, err = io.ReadFull(bytesMessageReader, prefix) - if err != nil { - return - } - s := packet.NewOCFBDecrypter(block, prefix, packet.OCFBResync) - if s == nil { - err = errors.New("pmapi: incorrect key for legacy decryption") - return - } - - reader := cipher.StreamReader{S: s, R: bytesMessageReader} - buf := new(bytes.Buffer) - _, _ = buf.ReadFrom(reader) - plaintextBytes := buf.Bytes() - - plaintext := "" - for i := 0; i < len(plaintextBytes); i++ { - plaintext += string(plaintextBytes[i]) - } - bytesPlaintext, err := decodeBase64UTF8(plaintext) - if err != nil { - return - } - - return bytesPlaintext, nil -} - -func decodeBase64UTF8(input string) (output []byte, err error) { - input = strings.TrimSpace(input) - decodedMessage, err := base64.StdEncoding.DecodeString(input) - if err != nil { - return - } - utf8DecodedMessage := []rune(string(decodedMessage)) - output = make([]byte, len(utf8DecodedMessage)) - for i := 0; i < len(utf8DecodedMessage); i++ { - output[i] = byte(int(utf8DecodedMessage[i])) - } - return -} - -func (m *Message) Encrypt(encrypter, signer *crypto.KeyRing) (err error) { - if m.IsBodyEncrypted() { - err = errors.New("pmapi: trying to encrypt an already encrypted message") - return - } - - m.Body, err = encrypt(encrypter, m.Body, signer) - return -} - -func (m *Message) Has(flag int64) bool { - return (m.Flags & flag) == flag -} - -func (m *Message) Recipients() []*mail.Address { - var recipients []*mail.Address - recipients = append(recipients, m.ToList...) - recipients = append(recipients, m.CCList...) - recipients = append(recipients, m.BCCList...) - return recipients -} - -// MessagesCount contains message counts for one label. -type MessagesCount struct { - LabelID string - Total int - Unread int -} - -// MessagesFilter contains fields to filter messages. -type MessagesFilter struct { - Page int - PageSize int - Limit int - LabelID string - Sort string // Time by default (Time, To, From, Subject, Size). - Desc *bool - Begin int64 // Unix time. - End int64 // Unix time. - BeginID string - EndID string - Keyword string - To string - From string - Subject string - ConversationID string - AddressID string - ID []string - Attachments *bool - Unread *bool - ExternalID string // MIME Message-Id (only valid for messages). - AutoWildcard *bool -} - -func (filter *MessagesFilter) urlValues() url.Values { //nolint:funlen - v := url.Values{} - - if filter.Page != 0 { - v.Set("Page", strconv.Itoa(filter.Page)) - } - if filter.PageSize != 0 { - v.Set("PageSize", strconv.Itoa(filter.PageSize)) - } - if filter.Limit != 0 { - v.Set("Limit", strconv.Itoa(filter.Limit)) - } - if filter.LabelID != "" { - v.Set("LabelID", filter.LabelID) - } - if filter.Sort != "" { - v.Set("Sort", filter.Sort) - } - if filter.Desc != nil { - if *filter.Desc { - v.Set("Desc", "1") - } else { - v.Set("Desc", "0") - } - } - if filter.Begin != 0 { - v.Set("Begin", strconv.Itoa(int(filter.Begin))) - } - if filter.End != 0 { - v.Set("End", strconv.Itoa(int(filter.End))) - } - if filter.BeginID != "" { - v.Set("BeginID", filter.BeginID) - } - if filter.EndID != "" { - v.Set("EndID", filter.EndID) - } - if filter.Keyword != "" { - v.Set("Keyword", filter.Keyword) - } - if filter.To != "" { - v.Set("To", filter.To) - } - if filter.From != "" { - v.Set("From", filter.From) - } - if filter.Subject != "" { - v.Set("Subject", filter.Subject) - } - if filter.ConversationID != "" { - v.Set("ConversationID", filter.ConversationID) - } - if filter.AddressID != "" { - v.Set("AddressID", filter.AddressID) - } - if len(filter.ID) > 0 { - for _, id := range filter.ID { - v.Add("ID[]", id) - } - } - if filter.Attachments != nil { - if *filter.Attachments { - v.Set("Attachments", "1") - } else { - v.Set("Attachments", "0") - } - } - if filter.Unread != nil { - if *filter.Unread { - v.Set("Unread", "1") - } else { - v.Set("Unread", "0") - } - } - if filter.ExternalID != "" { - v.Set("ExternalID", filter.ExternalID) - } - if filter.AutoWildcard != nil { - if *filter.AutoWildcard { - v.Set("AutoWildcard", "1") - } else { - v.Set("AutoWildcard", "0") - } - } - - return v -} - -// ListMessages gets message metadata. -func (c *client) ListMessages(ctx context.Context, filter *MessagesFilter) ([]*Message, int, error) { - var res struct { - Messages []*Message - Total int - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetQueryParamsFromValues(filter.urlValues()). - SetResult(&res). - Get("/mail/v4/messages") - }); err != nil { - return nil, 0, err - } - - return res.Messages, res.Total, nil -} - -// CountMessages counts messages by label. -func (c *client) CountMessages(ctx context.Context, addressID string) (counts []*MessagesCount, err error) { - var res struct { - Counts []*MessagesCount - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - if addressID != "" { - r = r.SetQueryParam("AddressID", addressID) - } - return r.SetResult(&res).Get("/mail/v4/messages/count") - }); err != nil { - return nil, err - } - - return res.Counts, nil -} - -// GetMessage retrieves a message. -func (c *client) GetMessage(ctx context.Context, messageID string) (msg *Message, err error) { - var res struct { - Message *Message - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&res).Get("/mail/v4/messages/" + messageID) - }); err != nil { - return nil, err - } - - return res.Message, nil -} - -type MessagesActionReq struct { - IDs []string -} - -func (c *client) MarkMessagesRead(ctx context.Context, messageIDs []string) error { - return doPaged(messageIDs, defaultPageSize, func(messageIDs []string) (err error) { - req := MessagesActionReq{IDs: messageIDs} - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(req).Put("/mail/v4/messages/read") - }); err != nil { - return err - } - - return nil - }) -} - -func (c *client) MarkMessagesUnread(ctx context.Context, messageIDs []string) error { - return doPaged(messageIDs, defaultPageSize, func(messageIDs []string) (err error) { - req := MessagesActionReq{IDs: messageIDs} - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(req).Put("/mail/v4/messages/unread") - }); err != nil { - return err - } - - return nil - }) -} - -func (c *client) DeleteMessages(ctx context.Context, messageIDs []string) error { - return doPaged(messageIDs, defaultPageSize, func(messageIDs []string) (err error) { - req := MessagesActionReq{IDs: messageIDs} - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(req).Put("/mail/v4/messages/delete") - }); err != nil { - return err - } - - return nil - }) -} - -func (c *client) UndeleteMessages(ctx context.Context, messageIDs []string) error { - return doPaged(messageIDs, defaultPageSize, func(messageIDs []string) (err error) { - req := MessagesActionReq{IDs: messageIDs} - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(req).Put("/mail/v4/messages/undelete") - }); err != nil { - return err - } - - return nil - }) -} - -type LabelMessagesReq struct { - LabelID string - IDs []string -} - -// LabelMessages labels the given message IDs with the given label. -// The requests are performed paged; this can eventually be done in parallel. -func (c *client) LabelMessages(ctx context.Context, messageIDs []string, labelID string) error { - return doPaged(messageIDs, defaultPageSize, func(messageIDs []string) (err error) { - req := LabelMessagesReq{ - LabelID: labelID, - IDs: messageIDs, - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(req).Put("/mail/v4/messages/label") - }); err != nil { - return err - } - - return nil - }) -} - -// UnlabelMessages removes the given label from the given message IDs. -// The requests are performed paged; this can eventually be done in parallel. -func (c *client) UnlabelMessages(ctx context.Context, messageIDs []string, labelID string) error { - return doPaged(messageIDs, defaultPageSize, func(messageIDs []string) (err error) { - req := LabelMessagesReq{ - LabelID: labelID, - IDs: messageIDs, - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetBody(req).Put("/mail/v4/messages/unlabel") - }); err != nil { - return err - } - - return nil - }) -} - -func (c *client) EmptyFolder(ctx context.Context, labelID, addressID string) error { - if labelID == "" { - return errors.New("labelID parameter is empty string") - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - if addressID != "" { - r.SetQueryParam("AddressID", addressID) - } - - return r.SetQueryParam("LabelID", labelID).Delete("/mail/v4/messages/empty") - }); err != nil { - return err - } - - return nil -} - -// ComputeMessageFlagsByLabels returns flags based on labels. -func ComputeMessageFlagsByLabels(labels []string) (flag int64) { - for _, labelID := range labels { - switch labelID { - case SentLabel: - flag = (flag | FlagSent) - case ArchiveLabel, InboxLabel: - flag = (flag | FlagReceived) - } - } - - // NOTE: if the labels are custom only - if flag == 0 { - flag = FlagReceived - } - - return flag -} diff --git a/pkg/pmapi/messages_test.go b/pkg/pmapi/messages_test.go deleted file mode 100644 index eed778c9..00000000 --- a/pkg/pmapi/messages_test.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "fmt" - "net/http" - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/require" -) - -const ( - testMessageCleartext = `
jeej saas

Sent from ProtonMail, encrypted email based in Switzerland.

` - testMessageCleartextLegacy = `
flkasjfkjasdklfjasd
fasd
jfasjdfjasd
fj
asdfj
sadjf
sadjf
asjdf
jasd
fj
asdjf
asdjfsad
fasdlkfjasdjfkljsadfljsdfjsdljflkdsjfkljsdlkfjsdlk
jasfd
jsd
jf
sdjfjsdf

djfskjsladf
asd
fja
sdjfajsf
jas
fas
fj
afj
ajf
af
asdfasdfasd
Sent from ProtonMail, encrypted email based in Switzerland.
dshfljsadfasdf
as
df
asd
fasd
f
asd
fasdflasdklfjsadlkjf
asd
fasdlkfjasdlkfjklasdjflkasjdflaslkfasdfjlasjflkasflksdjflkjasdf
asdflkasdjflajsfljaslkflasf
asdfkas
dfjas
djf
asjf
asj
faj
f
afj
sdjaf
jas
sdfj
ajf
aj
ajsdafafdaaf
a
f
lasl;ga
sags
ad
gags
g
ga
a
gg
a
ag
ag
agga.g.ga,ag.ag./ga

dsga
sg

gasga\g\g\g\g\g\n\y\t\r\\r\r\\n\n\n\


sd
asdf
asdf
dsa
fasd
f
` -) - -const testMessageEncrypted = `-----BEGIN PGP MESSAGE----- -Version: OpenPGP.js v1.2.0 -Comment: http://openpgpjs.org - -wcBMA0fcZ7XLgmf2AQf+JPulpEOWwmY/Sfze8rBpYvrO2cebSSkjCgapFfXG -CI4PA+rb+WGkn9uBJf3FgEEg76c2ZqGh9zXTyrdHyFLm8ekarvxzgLpvcei/ -p18IzcxsWnaM+1uknL4bKUtK3298gIl6xrfc4eVEA8tqUPUkSLSGk7uggjhj -zEYR4zIgMa0c6sMVcZ1Idvy9gGsTIvvcZJ4h1lKVUl8gba+qr1D76RaAf5xS -SBT74q9HhgfEMZwk6hXAp4MYY5h+lIsuhFu5kQ9fhZKU0PWS7ljddv854ZxS -9gHKPBerv4NBjkkCLp9xa2QNjDnu1fNlzlJpfCavp6wDdC83GiT61VRHPE4s -J9LASwFwgOrPmB8Mi867AQM0dddbj4Qe5ghlUcF1XnybkwfHqvQA1QT50d5n -ddFyxwIjvI/Nsn8MTCSnmrWCrjQ7v8JC73NyGxO5k6ZlUnc6BQVie78QJo5a -ftzl5b6nwlCYuXI8R6N/t5MXzrC5GwR8nvjH6kgbUVTLL1hO2Sbgyq5bBKLW -jjylTsZDHUGi4OX7q7eet5/RhKusWdvR0cHEaZAVD6BhTNN0mFBJ5bM1SINI -9gxJVqKJe7j4nJP4PGZBJrokZihhiBS/WEbJdvS54frYajGKjMavB3VhFP6k -qi5aiqGJKOJOV/G8yIwtdtxac3UL34eWo69U39Zx2mNfSXCzSjuafCr1nmAS -4g== -=Uw3B ------END PGP MESSAGE----- -` - -const testMessageEncryptedLegacy = `---BEGIN ENCRYPTED MESSAGE---esK5w7TCgVnDj8KQHBvDvhJObcOvw6/Cv2/CjMOpw5UES8KQwq/CiMOpI3MrexLDimzDmsKqVmwQw7vDkcKlRgXCosOpwoJgV8KEBCslSGbDtsOlw5gow7NxG8OSw6JNPlYuwrHCg8K5w6vDi8Kww5V5wo/Dl8KgwpnCi8Kww7nChMKdw5FHwoxmCGbCm8O6wpDDmRVEWsO7wqnCtVnDlMKORDbDnjbCqcOnNMKEwoPClFlaw6k1w5TDpcOGJsOUw5Unw5fCrcK3XnLCoRBBwo/DpsKAJiTDrUHDuGEQXz/DjMOhTCN7esO5ZjVIQSoFZMOyF8Kgw6nChcKmw6fCtcOBcW7Ck8KJwpTDnCzCnz3DjFY7wp5jUsOhw7XDosKQNsOUBmLDksKzPcO4fE/Dmw1GecKew4/CmcOJTFXDsB5uMcOFd1vDmX9ow4bDpCPDoU3Drw8oScKOXznDisKfYF3DvMKoEy0DDmzDhlHDjwIyC8OzRS/CnEZ4woM9w5cnw51fw6MZMAzDk8O3CDXDoyHDvzlFwqDCg8KsTnAiaMOsIyfCmUEaw6nChMK5TMOxG8KEHUNIwo1seMOXw5HDhyVawrzCr8KmFWHDpMO3asKpwrQbbMOlwoMew4t1Jz51wp9Jw6kGWcOzc8KgwpLCpsOHOMOgYB3DiMOxLcOQB8K7AcOyWF3CmnwfK8Kxw6XDm2TCiT/CnVTCg8Omw7Ngwp3CuUAHw6/CjRLDgcKsU8O/w6gXJ0cIw6pZMcOxEWETwpd4w58Mwr5SBMKORQjCi3FYcULDgx09w5M7SH7DrMKrw4gnXMKjwqUrBMOLwqQyF0nDhcKuwqTDqsO2w7LCnGjCvkbDgDgcw54xAkEiQMKUFlzDkMOew73CmkU4wrnCjw3DvsKaW8K0InA+w4sPSXfDuhbClMKgUcKeCMORw5ZYJcKnNEzDoMOhw7MYCX4DwqIQwoHCvsOaB1UAI8KVw6LCvcOTw53CuSgow4kZdHw5aRkYw7ZyV8OsP0LCh8KnwpIuw4p1NisoEcKcwrjDhcOtMzdvw5rDmsK3IAdAw7M4J8K+w6zCmR3CuMKUw4lqw6osPMObw53Dg8K3wqLCrsKZwr8mPcK4w4QWw5LCnwZeH1bDgwwiXcKbUhHDk1DDk0MLwoDDqMKXw5skNsKAAcOFw77Di8KNGCBzP8OcwrI5wodQQwQyw5V0wrInwrPDt8O+T8KbNsKVw7Mzw7HCsMOjwpcewoPCuMOUEsOow6QZVDjDpgbDlMOBGDXCtMOmw6jDuMKfw4nDlWTDq8Kqd0TDvwPCpSzDlA4JO3EHwrlBWcK5w7DCscOwCMK2wpsvwrYNIcOgBBXChMK0w6nCosKWEVd+w7cEal5hIcO4SWrCu0TDrW5Yw4XCmBgCwpc7YVwIwqPCi8OlGDzDmyJ/woHCscOtw4zDuC7CpUXCrDAJwp7Cj8KxPX3CrhDCvVB2w7PCosKbw7F+V11hY8Omwq1eQcO8w4wcRMKBJ2LDgW/DomXDhwkgAlxmQcKew6HDq8Ouw6ASeG/DlcKgUcKmLMOowpQWNcKJJcKDa3XDksK/woHCo3d6wrHDpMOqwqs/UUXCjUpnwrHCmsOyJx4bwoHChAnDi0TCpjLDrBvCvEghw5VtfhPCk8K5KsKIw75FCsOyDsKtV17CicOjwqAnF8OHHC0qMsOEwrgEwr13c8KZw4fDn8KXw73CksKAw4QTGRgIG8KMMXwpwrRBT2DDq8K3AsOQXl/DqMKYMivClsKiXcOhGkvDmsK9w77Cmmpvwrhsd8Kaw7bDgQ/DuCU2CyTDtjnCgn/DiMOtSyPDnsOfVTstccO6EVXDrj03MUHDvDDCgsO7BFQFEX3DszIyw7Rsw7pNwpjCs8OCLR9UbsOlw5USw73DiWJqVXTCl2tFw7FaAcKaw7l5a3Mvw5TCpMKCwpbDi3fCi8KHwrfDugUZwo5hw7fChsKDw5ZhPjA7w7HDjcO9wrrCjUbDoy4JXA1JICRDw49UNsOYOsK9FGE5wqhAw67DumnDqW0cwqbCu8OedEbDqcOfw50MVH8twpVLH8O3LsKvacKJw75xTMKkOcOJw4/DvsOYwqRwZcOnwqfCm2XCnRJFwqEgX8KLPsKfwpQWw6nChm82w6hME10KTRhGw5LCj1stPiXClsO8w7rCocOLw6lFw7tAZ8K0O3wswpZ4wqvCmMOFwpzDhMKVRRQjw53CikECPMOKZcOOwoAKcMK7WMO3K8Okw4bCjgrCisKLRsKewqzDvmtnw584wrtiw6RFVsKPecOpIhx7TsKzw4TCisKyw6nCqcK+w6fChsKxw5kWSsOgfD7CkRfCncKGKMOubsKoBA9Fe2YHwrx4aQNSG8Kpw5zDrMO1FMOPZcKSIVnDrHxOBsKyBcKmYwQMOl7CiRvCnDNVw7NaesOoPR3CrnQEwr9Xw600BSFYECnDgi1OFS7DoFYJw4M6wrzCog09WFPCmiHDogjDpQFjdsKKIsOWFsKXd0TDjXU3CsONRX3DssOrw4HDmX0Mw7rDiENvwpPCghsXacK2w6XCkMOICcKVw4nCkMO8RcOUw4zCn1VJw752RAUawqhdw5dEwqbDh0wAMH/DlTrChC/DosOoGsOPw5nClTcyw5XDlsKhNsKAcBINwpxUAi8Rw5Jvwpckwq4uBy0nw51dP2UGbidATX1FLMKFw5zDsQxewp3DlMKwwo3CrhBPJGR7cVHCnTUnwrDDksO0AcO5T3jCm245OnUVUT8WD1HDhTnCqnbCt8OjMDvCsAzCjsKSwoDDlDhtw7cFwpsDaS7CvVLDu0zDnlvDlMOEwrnCgVzCgcOZN8Oxwp0LSMKswq/DrMK9fcKTL1zDgcOvwofCtWAoL0IKR8OWwqpPw6QfVsKcwqxTXGEPKCFydX4Mw5jDmcOEWlPCgMKDPcOJw7HDgcOMahzCjMO7HyPDo8K3Y8OswqPDgSQ+w6wfw67Cr8O/w61oMsO+woTDrnECI2TDuMK5wrzDusOHw5/CosKFwrciQF3Csj5aw7DDpMKwZMK3Z8KlRBIcLcKvM2/CtBk8JMKWwqVyw6RNwoUhwoDCsXbCrD04wpQ4F8KOcMKIw7PDtMKqZRTCjsKSOMOKCMKYQ8OhwqZ1dGrChcKXLSnDiT7CrEjCihckNcOXw63CkUYpT8KTwq7CgMKiw7PCqmBzwq/Crz50XcKEGlLCrUBjw6ASVsObD8K9wpZ6eBHCi2FTMVcDSzvDgwtxw5ZJHlF5woDDtsKTwovChMOyYMKOSCt7w7hGDDsFaMOewrrCjRbDrGPDg2rCpsO3wo8IEMO9wqjCrG0mRXHDocKJwqQYdsKOw7UUwqIUwq/CqUlKW8ObwpcZGizCpgd4dAZBXMOYw5s5w6HDvkEgw6sbRxAwwoBSOyXCjDPDpsKlwrPCrl/DqsOswoJJDWzDp8Ocw5nDrE5FWm3DncKVwpnCqMKiwoDDmMONQcOEwpwRwonCsh0Tw7FCw6Nfw7U7wp7DnMKnfMOHCMOnw4TClcOVwrzCiiddUj3CmsOgwqvDhxfDjsOMWcKDZnvDocObw77Do1rDgMKHVsKCLcOXRMOHD0RNwpEdwozCrBnDqBYWwojCiVzCjTTCqcO5wqgAwqhhw7tnw5ZuOcOYNGTDiR1GAEzDuE0PeErDnlQlfsOjw6UGWUUNw6TCmgx8NMKzDMKgL8O3esKDwprDoTl8wrbDvVDCvU4Iw5sAwr/DugcoR8KMw4hNeMKSw7Jmw4rDjG8NbcO8w7jCs8OvfFXCoBBNfcOqNsK0EQLCncKPw53DrsOiwolvwqjCr8OZDsORw47DiyA+VcOMSg5wworDgGx0w7sgKMOyDMOyZRkgw43CqUHDicKfwpDCo8OII8KvKsOxDcKoFsOaw7HCgXTDssK7B8KIwoNcw4zCu8KBw4vCvFjDkWLDl8OyB8O/w4oYw5DCslzDk2kDw7jDgcOJw4jComXDkwdfw61xw53Cv8KPf11iwq0kKsKDw7nCmiVNF0NqLMKvwqvDjhQ3ZXbDomvDs8OKQQ7CocOnwr1Fw7xZRMK6w41cw5DDgzzCthIoAMOBQcOPbcOPVx/Cm8OYw7pHwo/CvCxhCcKVw7vChShnw6rClUQ7w6dbZMOrw4hpw7lZXMOxw5pnUXHDiMOLDxrDiA/DtMKqw6zDjXRJwp07BsKEwoTClBHCritDYXgzT3RWDcOlw4lfw4Vbw7fCj8K0w4AnwqjCrxPDpCVXF8KbY8OMPwQvwqdaw6E8w4AHPcKbNGl8wpQMX2PDp0pJfcOyGsOUXkNww5jCg8Obwo7DryjCisKeYiQ/XUzDvRvDncOtCMKJwqxHw6LDh8KwwrV7LGPCkcKOIXbCv8KHwpnDi1keQkLDssOSw7XCk8K+w7YdSMKAQmbDo8KPw7xywpnCsgANNTJYScKkNAvDo8KZw6Ayw6tmC8KaTsKEbcOZTx3DilrDtUjDi8OWV8K/wrocwpNKLlYbbcOmPcKPwrvCsTpLey5Xw58XJBPCo8KEPWJrwqZJX1fCncKDw4AZw4hWw5pTw7pidlzDtMO6w7t9DcK+R8KefMOfETvCskgjOgHCqcK7UgHCgsOfwrt8bcKQw5FeZcOiw4Faw7hRTjDDocOuEMOoEm04NQTCrCjDvMOaNDV6V8OHc8OTdMOndCh7HMOqw7HDnlzCl3MqwpjDiiDDtcKmCknCuBcQwobDvcOUN2LDmsOeHMOmPMKeH0nCt0nDgsO8w73CkRDDmMOuacO9w5J1KsKswqY7UMKyHHzDjMOjw5QOSWUhw4jCpMKJw4DCtcKNdcKPLcOFJsOqQ14=---END ENCRYPTED MESSAGE---||---BEGIN ENCRYPTED RANDOM KEY--------BEGIN PGP MESSAGE----- -Version: OpenPGP.js v0.9.0 -Comment: http://openpgpjs.org - -wcBMA2tjJVxNCRhtAQf/YzkQoUqaqa3NJ/c1apIF/dsl7yJ4GdVrC3/w7lxE -2CO5ioQD4s6QMWP2Y9dOdVl2INwz8eXOds9NS+1nMs4SoMbrpJnAjx8Cthti -1Z/8eWMU023LYahds8BYM0T435K/2tTB5GTA4uTl2y8Xzz2PbptQ4PrUDaII -+egeQQyPA0yuoRDwpaeTiaBYOSa06YYuK5Agr0buQAxRIMCxI2o+fucjoabv -FsQHKGu20U5GlJroSIyIVVkaH3evhNti/AnYX1HuokcGEQNsF5vo4SjWcH23 -2P86EIV+w5lUWC1FN9vZCyvbvyuqLHQMtqKVn4GBOkIc3bYQ0jru3a0FG4Cx -bNJ0ASps2+p3Vxe0d+so2iFV92ByQ+0skyCUwCNUlwOV5V5f2fy1ImXk4mXI -cO/bcbqRxx3pG9gkPIh43FoQktTT+tsJ5vS53qfaLGdhCYfkrWjsKu+2P9Xg -+Cr8clh6NTblhfkoAS1gzjA3XgsgEFrtP+OGqwg= -=c5WU ------END PGP MESSAGE----- ----END ENCRYPTED RANDOM KEY--- -` - -const testMessageSigned = `-----BEGIN PGP MESSAGE----- -Version: OpenPGP.js v4.5.3 -Comment: https://openpgpjs.org - -wcBMA0fcZ7XLgmf2AQgAgnHOlcAwVu2AnVfi2fIQHSkTQ0OFnZMJMRR3MJ1q -HtUW8jkSLcurL0Sn/tBFLIIR4YT2tQMzV7cvZzZyBEuZM4OYnDp8xSmoszPh -Gc/nvYG0A0pmKAQkL27v05Dul8oUWA0APT51urghH2Pzm7NdOMtTKIE4LQjS -mBfQ6Cf14uKV0xGS9v2dSFjFxxXEEpMQ+k60NCKRYClN2LVVxf3OKXbuugds -m2GUGn3CuFsiabosIUv4EcdE3aD9HbNo+PIWLJWRJIYJSc5+FWcbwXuIIFgC -XX1s7OV53ceZJnhjCmDE0N2ZOLLAYWED2zRvUa+CAqG+hZgc/3Ia+UmJUVuZ -BNLAugFuRsOVgh3olUIz0vazHhyGG0XIsNqmRm0U9SIfhWkPPHBmU6Xht6Qw -EvLbBfKTYHxX01yQUNgIv4S/TULeQuUjZQfsNYNXXGepS+jiCoIdEgUwpvre -OMFGsypwQXVCFYO/GQdYanMQRTckEexyBY4hGYVrevDM1yG/zGJIdbfI2L+1 -1cz76jI8PtzL+S0zcVkevLcjjsHm2Je959uSida9jara7Bymr0y56UdoXoWX -4vZ0kQNo58eEEV0zg7dit4lDvwcuSZMW6K//xNtRQ4QX7/EDtlcYqBJXPwJY -eQSBVeYbeUbZ+PHJdu5gbI85BJNE2dKcS1bdOhEU2lPLYpvmMpPdot9TwnJb -dN3l8yDyhScGvTIZqlxhU7HCM9VHAS0bDqCUoO8EruztUSgjMI+gKC9+xdVU -yrkF7K23UNLWflROMv4cp0LDRB57619Y2w5lY/MG5bS0jSfMWBwnJG2AF28c -2tYKnHw6rpZXvXnlDmEDT8suTzuTGA== -=Sir8 ------END PGP MESSAGE----- -` - -const testMessageSigner = `-----BEGIN PGP PUBLIC KEY BLOCK----- -Version: OpenPGP.js v0.7.1 -Comment: http://openpgpjs.org - -xsBNBFSI0BMBB/9td6B5RDzVSFTlFzYOS4JxIb5agtNW1rbA4FeLoC47bGLR -8E42IA6aKcO4H0vOZ1lFms0URiKk1DjCMXn3AUErbxqiV5IATRZLwliH6vwy -PI6j5rtGF8dyxYfwmLtoUNkDcPdcFEb4NCdowsN7e8tKU0bcpouZcQhAqawC -9nEdaG/gS5w+2k4hZX2lOKS1EF5SvP48UadlspEK2PLAIp5wB9XsFS9ey2wu -elzkSfDh7KUAlteqFGSMqIgYH62/gaKm+TcckfZeyiMHWFw6sfrcFQ3QOZPq -ahWt0Rn9XM5xBAxx5vW0oceuQ1vpvdfFlM5ix4gn/9w6MhmStaCee8/fABEB -AAHNBlVzZXJJRMLAcgQQAQgAJgUCVIjQHQYLCQgHAwIJEASDR1Fk7GNTBBUI -AgoDFgIBAhsDAh4BAADmhAf/Yt0mCfWqQ25NNGUN14pKKgnPm68zwj1SmMGa -pU7+7ItRpoFNaDwV5QYiQSLC1SvSb1ZeKoY928GPKfqYyJlBpTPL9zC1OHQj -9+2yYauHjYW9JWQM7hst2S2LBcdiQPOs3ybWPaO9yaccV4thxKOCPvyClaS5 -b9T4Iv9GEVZQIUvArkwI8hyzIi6skRgxflGheq1O+S1W4Gzt2VtYvo8g8r6W -GzAGMw2nrs2h0+vUr+dLDgIbFCTc5QU99d5jE/e5Hw8iqBxv9tqB1hVATf8T -wC8aU5MTtxtabOiBgG0PsBs6oIwjFqEjpOIza2/AflPZfo7stp6IiwbwvTHo -1NlHoM7ATQRUiNAdAQf/eOLJYxX4lUQUzrNQgASDNE8gJPj7ywcGzySyqr0Y -5rbG57EjtKMIgZrpzJRpSCuRbBjfsltqJ5Q9TBAbPO+oR3rue0LqPKMnmr/q -KsHswBJRfsb/dbktUNmv/f7R9IVyOuvyP6RgdGeloxdGNeWiZSA6AZYI+WGc -xaOvVDPz8thtnML4G4MUhXxxNZ7JzQ0Lfz6mN8CCkblIP5xpcJsyRU7lUsGD -EJGZX0JH/I8bRVN1Xu08uFinIkZyiXRJ5ZGgF3Dns6VbIWmbttY54tBELtk+ -5g9pNSl9qiYwiCdwuZrA//NmD3xlZIN8sG4eM7ZUibZ23vEq+bUt1++6Mpba -GQARAQABwsBfBBgBCAATBQJUiNAfCRAEg0dRZOxjUwIbDAAAlpMH/085qZdO -mGRAlbvViUNhF2rtHvCletC48WHGO1ueSh9VTxalkP21YAYLJ4JgJzArJ7tH -lEeiKiHm8YU9KhLe11Yv/o3AiKIAQjJiQluvk+mWdMcddB4fBjL6ttMTRAXe -gHnjtMoamHbSZdeUTUadv05Fl6ivWtpXlODG4V02YvDiGBUbDosdGXEqDtpT -g6MYlj3QMvUiUNQvt7YGMJS8A9iQ9qBNzErgRW8L6CON2RmpQ/wgwP5nwUHz -JjY51d82Vj8bZeI8LdsX41SPoUhyC7kmNYpw9ZRy7NlrCt8dBIOB4/BKEJ2G -ClW54lp9eeOfYTsdTSbn9VaSO0E6m2/Q4Tk= -=WFtr ------END PGP PUBLIC KEY BLOCK-----` - -func TestMessage_IsBodyEncrypted(t *testing.T) { - r := require.New(t) - msg := &Message{Body: testMessageEncrypted} - r.True(msg.IsBodyEncrypted(), "the body should be encrypted") - - msg.Body = testMessageCleartext - r.True(!msg.IsBodyEncrypted(), "the body should not be encrypted") -} - -func TestMessage_Decrypt(t *testing.T) { - r := require.New(t) - msg := &Message{Body: testMessageEncrypted} - dec, err := msg.Decrypt(testPrivateKeyRing) - r.NoError(err) - r.Equal(testMessageCleartext, string(dec)) -} - -func TestMessage_Decrypt_Legacy(t *testing.T) { - r := require.New(t) - testPrivateKeyLegacy := readTestFile("testPrivateKeyLegacy", false) - - key, err := crypto.NewKeyFromArmored(testPrivateKeyLegacy) - r.NoError(err) - - unlockedKey, err := key.Unlock([]byte(testMailboxPasswordLegacy)) - r.NoError(err) - - testPrivateKeyRingLegacy, err := crypto.NewKeyRing(unlockedKey) - r.NoError(err) - - msg := &Message{Body: testMessageEncryptedLegacy} - - dec, err := msg.Decrypt(testPrivateKeyRingLegacy) - r.NoError(err) - - r.Equal(testMessageCleartextLegacy, string(dec)) -} - -func TestMessage_Decrypt_signed(t *testing.T) { - r := require.New(t) - msg := &Message{Body: testMessageSigned} - dec, err := msg.Decrypt(testPrivateKeyRing) - r.NoError(err) - r.Equal(testMessageCleartext, string(dec)) -} - -func TestMessage_Encrypt(t *testing.T) { - r := require.New(t) - - key, err := crypto.NewKeyFromArmored(testMessageSigner) - r.NoError(err) - - signer, err := crypto.NewKeyRing(key) - r.NoError(err) - - msg := &Message{Body: testMessageCleartext} - r.NoError(msg.Encrypt(testPrivateKeyRing, testPrivateKeyRing)) - - dec, err := msg.Decrypt(testPrivateKeyRing) - r.NoError(err) - - r.Equal(testMessageCleartext, string(dec)) - r.Equal(testIdentity, signer.GetIdentities()[0]) -} - -func routeLabelMessages(tb testing.TB, w http.ResponseWriter, req *http.Request) string { - require.NoError(tb, checkMethodAndPath(req, "PUT", "/mail/v4/messages/label")) - return "messages/label/put_response.json" -} - -func TestMessage_LabelMessages_NoPaging(t *testing.T) { - r := require.New(t) - - // This should be only enough IDs to produce one page. - testIDs := []string{} - for i := 0; i < messageIDPageSize-1; i++ { - testIDs = append(testIDs, fmt.Sprintf("%v", i)) - } - - // There should be enough IDs to produce just one page so the endpoint should be called once. - finish, c := newTestClientCallbacks(t, - routeLabelMessages, - ) - defer finish() - - r.NoError(c.LabelMessages(context.Background(), testIDs, "mylabel")) -} - -func TestMessage_LabelMessages_Paging(t *testing.T) { - r := require.New(t) - - // This should be enough IDs to produce three pages. - testIDs := []string{} - for i := 0; i < 3*messageIDPageSize; i++ { - testIDs = append(testIDs, fmt.Sprintf("%v", i)) - } - - // There should be enough IDs to produce three pages so the endpoint should be called three times. - finish, c := newTestClientCallbacks(t, - routeLabelMessages, - routeLabelMessages, - routeLabelMessages, - ) - defer finish() - - r.NoError(c.LabelMessages(context.Background(), testIDs, "mylabel")) -} - -// TestClient_GetMessage might look like no actual functionality is tested -// here. But there was case when API was responding with bad payload and it was -// useful to have this to quickly test it. -func TestClient_GetMessage(t *testing.T) { - r := require.New(t) - testID := "AeUizgtA3H44qRgcr-HdBApwLiUhlQg5kB81mg_QalWotmQJIHep9OScWIo7Wu9pnYxM4RqQxJnr3BE4kh4y_Q==" - - finish, c := newTestClientCallbacks(t, - func(tb testing.TB, w http.ResponseWriter, req *http.Request) string { - r.NoError(checkMethodAndPath(req, "GET", "/mail/v4/messages/"+testID)) - - return "/messages/get_response.json" - }, - ) - defer finish() - - msg, err := c.GetMessage(context.Background(), testID) - r.NoError(err) - r.Equal(testID, msg.ID) -} diff --git a/pkg/pmapi/mocks/mocks.go b/pkg/pmapi/mocks/mocks.go deleted file mode 100644 index 085cbe5c..00000000 --- a/pkg/pmapi/mocks/mocks.go +++ /dev/null @@ -1,883 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ProtonMail/proton-bridge/v2/pkg/pmapi (interfaces: Client,Manager) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - context "context" - io "io" - http "net/http" - reflect "reflect" - time "time" - - crypto "github.com/ProtonMail/gopenpgp/v2/crypto" - pmapi "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - gomock "github.com/golang/mock/gomock" - logrus "github.com/sirupsen/logrus" -) - -// MockClient is a mock of Client interface. -type MockClient struct { - ctrl *gomock.Controller - recorder *MockClientMockRecorder -} - -// MockClientMockRecorder is the mock recorder for MockClient. -type MockClientMockRecorder struct { - mock *MockClient -} - -// NewMockClient creates a new mock instance. -func NewMockClient(ctrl *gomock.Controller) *MockClient { - mock := &MockClient{ctrl: ctrl} - mock.recorder = &MockClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockClient) EXPECT() *MockClientMockRecorder { - return m.recorder -} - -// AddAuthRefreshHandler mocks base method. -func (m *MockClient) AddAuthRefreshHandler(arg0 pmapi.AuthRefreshHandler) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "AddAuthRefreshHandler", arg0) -} - -// AddAuthRefreshHandler indicates an expected call of AddAuthRefreshHandler. -func (mr *MockClientMockRecorder) AddAuthRefreshHandler(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAuthRefreshHandler", reflect.TypeOf((*MockClient)(nil).AddAuthRefreshHandler), arg0) -} - -// Addresses mocks base method. -func (m *MockClient) Addresses() pmapi.AddressList { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Addresses") - ret0, _ := ret[0].(pmapi.AddressList) - return ret0 -} - -// Addresses indicates an expected call of Addresses. -func (mr *MockClientMockRecorder) Addresses() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addresses", reflect.TypeOf((*MockClient)(nil).Addresses)) -} - -// Auth2FA mocks base method. -func (m *MockClient) Auth2FA(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Auth2FA", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Auth2FA indicates an expected call of Auth2FA. -func (mr *MockClientMockRecorder) Auth2FA(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth2FA", reflect.TypeOf((*MockClient)(nil).Auth2FA), arg0, arg1) -} - -// AuthDelete mocks base method. -func (m *MockClient) AuthDelete(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AuthDelete", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// AuthDelete indicates an expected call of AuthDelete. -func (mr *MockClientMockRecorder) AuthDelete(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthDelete", reflect.TypeOf((*MockClient)(nil).AuthDelete), arg0) -} - -// AuthSalt mocks base method. -func (m *MockClient) AuthSalt(arg0 context.Context) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AuthSalt", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AuthSalt indicates an expected call of AuthSalt. -func (mr *MockClientMockRecorder) AuthSalt(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthSalt", reflect.TypeOf((*MockClient)(nil).AuthSalt), arg0) -} - -// CountMessages mocks base method. -func (m *MockClient) CountMessages(arg0 context.Context, arg1 string) ([]*pmapi.MessagesCount, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CountMessages", arg0, arg1) - ret0, _ := ret[0].([]*pmapi.MessagesCount) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CountMessages indicates an expected call of CountMessages. -func (mr *MockClientMockRecorder) CountMessages(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountMessages", reflect.TypeOf((*MockClient)(nil).CountMessages), arg0, arg1) -} - -// CreateAttachment mocks base method. -func (m *MockClient) CreateAttachment(arg0 context.Context, arg1 *pmapi.Attachment, arg2, arg3 io.Reader) (*pmapi.Attachment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateAttachment", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*pmapi.Attachment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateAttachment indicates an expected call of CreateAttachment. -func (mr *MockClientMockRecorder) CreateAttachment(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAttachment", reflect.TypeOf((*MockClient)(nil).CreateAttachment), arg0, arg1, arg2, arg3) -} - -// CreateDraft mocks base method. -func (m *MockClient) CreateDraft(arg0 context.Context, arg1 *pmapi.Message, arg2 string, arg3 int) (*pmapi.Message, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateDraft", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*pmapi.Message) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateDraft indicates an expected call of CreateDraft. -func (mr *MockClientMockRecorder) CreateDraft(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDraft", reflect.TypeOf((*MockClient)(nil).CreateDraft), arg0, arg1, arg2, arg3) -} - -// CreateLabel mocks base method. -func (m *MockClient) CreateLabel(arg0 context.Context, arg1 *pmapi.Label) (*pmapi.Label, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateLabel", arg0, arg1) - ret0, _ := ret[0].(*pmapi.Label) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateLabel indicates an expected call of CreateLabel. -func (mr *MockClientMockRecorder) CreateLabel(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLabel", reflect.TypeOf((*MockClient)(nil).CreateLabel), arg0, arg1) -} - -// CreateLabelV4 mocks base method. -func (m *MockClient) CreateLabelV4(arg0 context.Context, arg1 *pmapi.Label) (*pmapi.Label, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateLabelV4", arg0, arg1) - ret0, _ := ret[0].(*pmapi.Label) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateLabelV4 indicates an expected call of CreateLabelV4. -func (mr *MockClientMockRecorder) CreateLabelV4(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLabelV4", reflect.TypeOf((*MockClient)(nil).CreateLabelV4), arg0, arg1) -} - -// CurrentUser mocks base method. -func (m *MockClient) CurrentUser(arg0 context.Context) (*pmapi.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CurrentUser", arg0) - ret0, _ := ret[0].(*pmapi.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CurrentUser indicates an expected call of CurrentUser. -func (mr *MockClientMockRecorder) CurrentUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentUser", reflect.TypeOf((*MockClient)(nil).CurrentUser), arg0) -} - -// DecryptAndVerifyCards mocks base method. -func (m *MockClient) DecryptAndVerifyCards(arg0 []pmapi.Card) ([]pmapi.Card, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DecryptAndVerifyCards", arg0) - ret0, _ := ret[0].([]pmapi.Card) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DecryptAndVerifyCards indicates an expected call of DecryptAndVerifyCards. -func (mr *MockClientMockRecorder) DecryptAndVerifyCards(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptAndVerifyCards", reflect.TypeOf((*MockClient)(nil).DecryptAndVerifyCards), arg0) -} - -// DeleteLabel mocks base method. -func (m *MockClient) DeleteLabel(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteLabel", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteLabel indicates an expected call of DeleteLabel. -func (mr *MockClientMockRecorder) DeleteLabel(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabel", reflect.TypeOf((*MockClient)(nil).DeleteLabel), arg0, arg1) -} - -// DeleteLabelV4 mocks base method. -func (m *MockClient) DeleteLabelV4(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteLabelV4", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteLabelV4 indicates an expected call of DeleteLabelV4. -func (mr *MockClientMockRecorder) DeleteLabelV4(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabelV4", reflect.TypeOf((*MockClient)(nil).DeleteLabelV4), arg0, arg1) -} - - -// DeleteMessages mocks base method. -func (m *MockClient) DeleteMessages(arg0 context.Context, arg1 []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteMessages", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteMessages indicates an expected call of DeleteMessages. -func (mr *MockClientMockRecorder) DeleteMessages(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessages", reflect.TypeOf((*MockClient)(nil).DeleteMessages), arg0, arg1) -} - -// EmptyFolder mocks base method. -func (m *MockClient) EmptyFolder(arg0 context.Context, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EmptyFolder", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// EmptyFolder indicates an expected call of EmptyFolder. -func (mr *MockClientMockRecorder) EmptyFolder(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EmptyFolder", reflect.TypeOf((*MockClient)(nil).EmptyFolder), arg0, arg1, arg2) -} - -// GetAddresses mocks base method. -func (m *MockClient) GetAddresses(arg0 context.Context) (pmapi.AddressList, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAddresses", arg0) - ret0, _ := ret[0].(pmapi.AddressList) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAddresses indicates an expected call of GetAddresses. -func (mr *MockClientMockRecorder) GetAddresses(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAddresses", reflect.TypeOf((*MockClient)(nil).GetAddresses), arg0) -} - -// GetAttachment mocks base method. -func (m *MockClient) GetAttachment(arg0 context.Context, arg1 string) (io.ReadCloser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAttachment", arg0, arg1) - ret0, _ := ret[0].(io.ReadCloser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAttachment indicates an expected call of GetAttachment. -func (mr *MockClientMockRecorder) GetAttachment(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockClient)(nil).GetAttachment), arg0, arg1) -} - -// GetContactByID mocks base method. -func (m *MockClient) GetContactByID(arg0 context.Context, arg1 string) (pmapi.Contact, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetContactByID", arg0, arg1) - ret0, _ := ret[0].(pmapi.Contact) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetContactByID indicates an expected call of GetContactByID. -func (mr *MockClientMockRecorder) GetContactByID(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactByID", reflect.TypeOf((*MockClient)(nil).GetContactByID), arg0, arg1) -} - -// GetContactEmailByEmail mocks base method. -func (m *MockClient) GetContactEmailByEmail(arg0 context.Context, arg1 string, arg2, arg3 int) ([]pmapi.ContactEmail, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetContactEmailByEmail", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]pmapi.ContactEmail) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetContactEmailByEmail indicates an expected call of GetContactEmailByEmail. -func (mr *MockClientMockRecorder) GetContactEmailByEmail(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactEmailByEmail", reflect.TypeOf((*MockClient)(nil).GetContactEmailByEmail), arg0, arg1, arg2, arg3) -} - -// GetEvent mocks base method. -func (m *MockClient) GetEvent(arg0 context.Context, arg1 string) (*pmapi.Event, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEvent", arg0, arg1) - ret0, _ := ret[0].(*pmapi.Event) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetEvent indicates an expected call of GetEvent. -func (mr *MockClientMockRecorder) GetEvent(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvent", reflect.TypeOf((*MockClient)(nil).GetEvent), arg0, arg1) -} - -// GetMailSettings mocks base method. -func (m *MockClient) GetMailSettings(arg0 context.Context) (pmapi.MailSettings, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMailSettings", arg0) - ret0, _ := ret[0].(pmapi.MailSettings) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMailSettings indicates an expected call of GetMailSettings. -func (mr *MockClientMockRecorder) GetMailSettings(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailSettings", reflect.TypeOf((*MockClient)(nil).GetMailSettings), arg0) -} - -// GetMessage mocks base method. -func (m *MockClient) GetMessage(arg0 context.Context, arg1 string) (*pmapi.Message, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMessage", arg0, arg1) - ret0, _ := ret[0].(*pmapi.Message) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMessage indicates an expected call of GetMessage. -func (mr *MockClientMockRecorder) GetMessage(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockClient)(nil).GetMessage), arg0, arg1) -} - -// GetPublicKeysForEmail mocks base method. -func (m *MockClient) GetPublicKeysForEmail(arg0 context.Context, arg1 string) ([]pmapi.PublicKey, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPublicKeysForEmail", arg0, arg1) - ret0, _ := ret[0].([]pmapi.PublicKey) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetPublicKeysForEmail indicates an expected call of GetPublicKeysForEmail. -func (mr *MockClientMockRecorder) GetPublicKeysForEmail(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeysForEmail", reflect.TypeOf((*MockClient)(nil).GetPublicKeysForEmail), arg0, arg1) -} - -// GetUser mocks base method. -func (m *MockClient) GetUser(arg0 context.Context) (*pmapi.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUser", arg0) - ret0, _ := ret[0].(*pmapi.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUser indicates an expected call of GetUser. -func (mr *MockClientMockRecorder) GetUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockClient)(nil).GetUser), arg0) -} - -// GetUserKeyRing mocks base method. -func (m *MockClient) GetUserKeyRing() (*crypto.KeyRing, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserKeyRing") - ret0, _ := ret[0].(*crypto.KeyRing) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserKeyRing indicates an expected call of GetUserKeyRing. -func (mr *MockClientMockRecorder) GetUserKeyRing() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserKeyRing", reflect.TypeOf((*MockClient)(nil).GetUserKeyRing)) -} - -// Import mocks base method. -func (m *MockClient) Import(arg0 context.Context, arg1 pmapi.ImportMsgReqs) ([]*pmapi.ImportMsgRes, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Import", arg0, arg1) - ret0, _ := ret[0].([]*pmapi.ImportMsgRes) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Import indicates an expected call of Import. -func (mr *MockClientMockRecorder) Import(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*MockClient)(nil).Import), arg0, arg1) -} - -// IsUnlocked mocks base method. -func (m *MockClient) IsUnlocked() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsUnlocked") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsUnlocked indicates an expected call of IsUnlocked. -func (mr *MockClientMockRecorder) IsUnlocked() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUnlocked", reflect.TypeOf((*MockClient)(nil).IsUnlocked)) -} - -// KeyRingForAddressID mocks base method. -func (m *MockClient) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0) - ret0, _ := ret[0].(*crypto.KeyRing) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// KeyRingForAddressID indicates an expected call of KeyRingForAddressID. -func (mr *MockClientMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockClient)(nil).KeyRingForAddressID), arg0) -} - -// LabelMessages mocks base method. -func (m *MockClient) LabelMessages(arg0 context.Context, arg1 []string, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LabelMessages", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// LabelMessages indicates an expected call of LabelMessages. -func (mr *MockClientMockRecorder) LabelMessages(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelMessages", reflect.TypeOf((*MockClient)(nil).LabelMessages), arg0, arg1, arg2) -} - -// ListLabels mocks base method. -func (m *MockClient) ListLabels(arg0 context.Context) ([]*pmapi.Label, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListLabels", arg0) - ret0, _ := ret[0].([]*pmapi.Label) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListLabels indicates an expected call of ListLabels. -func (mr *MockClientMockRecorder) ListLabels(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLabels", reflect.TypeOf((*MockClient)(nil).ListLabels), arg0) -} - -// ListLabelsOnly mocks base method. -func (m *MockClient) ListLabelsOnly(arg0 context.Context) ([]*pmapi.Label, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListLabelsOnly", arg0) - ret0, _ := ret[0].([]*pmapi.Label) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListLabelsOnly indicates an expected call of ListLabelsOnly. -func (mr *MockClientMockRecorder) ListLabelsOnly(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLabelsOnly", reflect.TypeOf((*MockClient)(nil).ListLabelsOnly), arg0) -} - -// ListFoldersOnly mocks base method. -func (m *MockClient) ListFoldersOnly(arg0 context.Context) ([]*pmapi.Label, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListFoldersOnly", arg0) - ret0, _ := ret[0].([]*pmapi.Label) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListFoldersOnly indicates an expected call of ListFoldersOnly. -func (mr *MockClientMockRecorder) ListFoldersOnly(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFoldersOnly", reflect.TypeOf((*MockClient)(nil).ListFoldersOnly), arg0) -} - -// ListMessages mocks base method. -func (m *MockClient) ListMessages(arg0 context.Context, arg1 *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListMessages", arg0, arg1) - ret0, _ := ret[0].([]*pmapi.Message) - ret1, _ := ret[1].(int) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// ListMessages indicates an expected call of ListMessages. -func (mr *MockClientMockRecorder) ListMessages(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMessages", reflect.TypeOf((*MockClient)(nil).ListMessages), arg0, arg1) -} - -// MarkMessagesRead mocks base method. -func (m *MockClient) MarkMessagesRead(arg0 context.Context, arg1 []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MarkMessagesRead", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// MarkMessagesRead indicates an expected call of MarkMessagesRead. -func (mr *MockClientMockRecorder) MarkMessagesRead(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesRead", reflect.TypeOf((*MockClient)(nil).MarkMessagesRead), arg0, arg1) -} - -// MarkMessagesUnread mocks base method. -func (m *MockClient) MarkMessagesUnread(arg0 context.Context, arg1 []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MarkMessagesUnread", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// MarkMessagesUnread indicates an expected call of MarkMessagesUnread. -func (mr *MockClientMockRecorder) MarkMessagesUnread(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesUnread", reflect.TypeOf((*MockClient)(nil).MarkMessagesUnread), arg0, arg1) -} - -// ReloadKeys mocks base method. -func (m *MockClient) ReloadKeys(arg0 context.Context, arg1 []byte) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReloadKeys", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// ReloadKeys indicates an expected call of ReloadKeys. -func (mr *MockClientMockRecorder) ReloadKeys(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReloadKeys", reflect.TypeOf((*MockClient)(nil).ReloadKeys), arg0, arg1) -} - -// ReorderAddresses mocks base method. -func (m *MockClient) ReorderAddresses(arg0 context.Context, arg1 []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReorderAddresses", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// ReorderAddresses indicates an expected call of ReorderAddresses. -func (mr *MockClientMockRecorder) ReorderAddresses(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderAddresses", reflect.TypeOf((*MockClient)(nil).ReorderAddresses), arg0, arg1) -} - -// SendMessage mocks base method. -func (m *MockClient) SendMessage(arg0 context.Context, arg1 string, arg2 *pmapi.SendMessageReq) (*pmapi.Message, *pmapi.Message, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendMessage", arg0, arg1, arg2) - ret0, _ := ret[0].(*pmapi.Message) - ret1, _ := ret[1].(*pmapi.Message) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// SendMessage indicates an expected call of SendMessage. -func (mr *MockClientMockRecorder) SendMessage(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockClient)(nil).SendMessage), arg0, arg1, arg2) -} - -// UnlabelMessages mocks base method. -func (m *MockClient) UnlabelMessages(arg0 context.Context, arg1 []string, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UnlabelMessages", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// UnlabelMessages indicates an expected call of UnlabelMessages. -func (mr *MockClientMockRecorder) UnlabelMessages(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlabelMessages", reflect.TypeOf((*MockClient)(nil).UnlabelMessages), arg0, arg1, arg2) -} - -// Unlock mocks base method. -func (m *MockClient) Unlock(arg0 context.Context, arg1 []byte) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Unlock", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Unlock indicates an expected call of Unlock. -func (mr *MockClientMockRecorder) Unlock(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockClient)(nil).Unlock), arg0, arg1) -} - -// UpdateLabel mocks base method. -func (m *MockClient) UpdateLabel(arg0 context.Context, arg1 *pmapi.Label) (*pmapi.Label, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateLabel", arg0, arg1) - ret0, _ := ret[0].(*pmapi.Label) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateLabel indicates an expected call of UpdateLabel. -func (mr *MockClientMockRecorder) UpdateLabel(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLabel", reflect.TypeOf((*MockClient)(nil).UpdateLabel), arg0, arg1) -} - -// UpdateLabelV4 mocks base method. -func (m *MockClient) UpdateLabelV4(arg0 context.Context, arg1 *pmapi.Label) (*pmapi.Label, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateLabelV4", arg0, arg1) - ret0, _ := ret[0].(*pmapi.Label) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateLabelV4 indicates an expected call of UpdateLabelV4. -func (mr *MockClientMockRecorder) UpdateLabelV4(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLabelUpdateLabelV4", reflect.TypeOf((*MockClient)(nil).UpdateLabelV4), arg0, arg1) -} - -// UpdateUser mocks base method. -func (m *MockClient) UpdateUser(arg0 context.Context) (*pmapi.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUser", arg0) - ret0, _ := ret[0].(*pmapi.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateUser indicates an expected call of UpdateUser. -func (mr *MockClientMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockClient)(nil).UpdateUser), arg0) -} - -// MockManager is a mock of Manager interface. -type MockManager struct { - ctrl *gomock.Controller - recorder *MockManagerMockRecorder -} - -// MockManagerMockRecorder is the mock recorder for MockManager. -type MockManagerMockRecorder struct { - mock *MockManager -} - -// NewMockManager creates a new mock instance. -func NewMockManager(ctrl *gomock.Controller) *MockManager { - mock := &MockManager{ctrl: ctrl} - mock.recorder = &MockManagerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockManager) EXPECT() *MockManagerMockRecorder { - return m.recorder -} - -// AddConnectionObserver mocks base method. -func (m *MockManager) AddConnectionObserver(arg0 pmapi.ConnectionObserver) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "AddConnectionObserver", arg0) -} - -// AddConnectionObserver indicates an expected call of AddConnectionObserver. -func (mr *MockManagerMockRecorder) AddConnectionObserver(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddConnectionObserver", reflect.TypeOf((*MockManager)(nil).AddConnectionObserver), arg0) -} - -// AllowProxy mocks base method. -func (m *MockManager) AllowProxy() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "AllowProxy") -} - -// AllowProxy indicates an expected call of AllowProxy. -func (mr *MockManagerMockRecorder) AllowProxy() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowProxy", reflect.TypeOf((*MockManager)(nil).AllowProxy)) -} - -// DisallowProxy mocks base method. -func (m *MockManager) DisallowProxy() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "DisallowProxy") -} - -// DisallowProxy indicates an expected call of DisallowProxy. -func (mr *MockManagerMockRecorder) DisallowProxy() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisallowProxy", reflect.TypeOf((*MockManager)(nil).DisallowProxy)) -} - -// DownloadAndVerify mocks base method. -func (m *MockManager) DownloadAndVerify(arg0 *crypto.KeyRing, arg1, arg2 string) ([]byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DownloadAndVerify", arg0, arg1, arg2) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DownloadAndVerify indicates an expected call of DownloadAndVerify. -func (mr *MockManagerMockRecorder) DownloadAndVerify(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadAndVerify", reflect.TypeOf((*MockManager)(nil).DownloadAndVerify), arg0, arg1, arg2) -} - -// NewClient mocks base method. -func (m *MockManager) NewClient(arg0, arg1, arg2 string, arg3 time.Time) pmapi.Client { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewClient", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(pmapi.Client) - return ret0 -} - -// NewClient indicates an expected call of NewClient. -func (mr *MockManagerMockRecorder) NewClient(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockManager)(nil).NewClient), arg0, arg1, arg2, arg3) -} - -// NewClientWithLogin mocks base method. -func (m *MockManager) NewClientWithLogin(arg0 context.Context, arg1 string, arg2 []byte) (pmapi.Client, *pmapi.Auth, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewClientWithLogin", arg0, arg1, arg2) - ret0, _ := ret[0].(pmapi.Client) - ret1, _ := ret[1].(*pmapi.Auth) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// NewClientWithLogin indicates an expected call of NewClientWithLogin. -func (mr *MockManagerMockRecorder) NewClientWithLogin(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClientWithLogin", reflect.TypeOf((*MockManager)(nil).NewClientWithLogin), arg0, arg1, arg2) -} - -// NewClientWithRefresh mocks base method. -func (m *MockManager) NewClientWithRefresh(arg0 context.Context, arg1, arg2 string) (pmapi.Client, *pmapi.AuthRefresh, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewClientWithRefresh", arg0, arg1, arg2) - ret0, _ := ret[0].(pmapi.Client) - ret1, _ := ret[1].(*pmapi.AuthRefresh) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// NewClientWithRefresh indicates an expected call of NewClientWithRefresh. -func (mr *MockManagerMockRecorder) NewClientWithRefresh(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClientWithRefresh", reflect.TypeOf((*MockManager)(nil).NewClientWithRefresh), arg0, arg1, arg2) -} - -// ReportBug mocks base method. -func (m *MockManager) ReportBug(arg0 context.Context, arg1 pmapi.ReportBugReq) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReportBug", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// ReportBug indicates an expected call of ReportBug. -func (mr *MockManagerMockRecorder) ReportBug(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBug", reflect.TypeOf((*MockManager)(nil).ReportBug), arg0, arg1) -} - -// SendSimpleMetric mocks base method. -func (m *MockManager) SendSimpleMetric(arg0 context.Context, arg1, arg2, arg3 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendSimpleMetric", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// SendSimpleMetric indicates an expected call of SendSimpleMetric. -func (mr *MockManagerMockRecorder) SendSimpleMetric(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendSimpleMetric", reflect.TypeOf((*MockManager)(nil).SendSimpleMetric), arg0, arg1, arg2, arg3) -} - -// SetCookieJar mocks base method. -func (m *MockManager) SetCookieJar(arg0 http.CookieJar) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetCookieJar", arg0) -} - -// SetCookieJar indicates an expected call of SetCookieJar. -func (mr *MockManagerMockRecorder) SetCookieJar(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCookieJar", reflect.TypeOf((*MockManager)(nil).SetCookieJar), arg0) -} - -// SetLogging mocks base method. -func (m *MockManager) SetLogging(arg0 *logrus.Entry, arg1 bool) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetLogging", arg0, arg1) -} - -// SetLogging indicates an expected call of SetLogging. -func (mr *MockManagerMockRecorder) SetLogging(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogging", reflect.TypeOf((*MockManager)(nil).SetLogging), arg0, arg1) -} - -// SetRetryCount mocks base method. -func (m *MockManager) SetRetryCount(arg0 int) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetRetryCount", arg0) -} - -// SetRetryCount indicates an expected call of SetRetryCount. -func (mr *MockManagerMockRecorder) SetRetryCount(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRetryCount", reflect.TypeOf((*MockManager)(nil).SetRetryCount), arg0) -} - -// SetTransport mocks base method. -func (m *MockManager) SetTransport(arg0 http.RoundTripper) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetTransport", arg0) -} - -// SetTransport indicates an expected call of SetTransport. -func (mr *MockManagerMockRecorder) SetTransport(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTransport", reflect.TypeOf((*MockManager)(nil).SetTransport), arg0) -} diff --git a/pkg/pmapi/observer.go b/pkg/pmapi/observer.go deleted file mode 100644 index f710516c..00000000 --- a/pkg/pmapi/observer.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -type ConnectionObserver interface { - OnDown() - OnUp() -} - -type observer struct { - onDown, onUp func() -} - -// NewConnectionObserver is a helper function to create a new connection observer from two callbacks. -// It doesn't need to be used; anything which implements the ConnectionObserver interface can be an observer. -func NewConnectionObserver(onDown, onUp func()) ConnectionObserver { - return &observer{ - onDown: onDown, - onUp: onUp, - } -} - -func (o observer) OnDown() { o.onDown() } - -func (o observer) OnUp() { o.onUp() } diff --git a/pkg/pmapi/paging.go b/pkg/pmapi/paging.go deleted file mode 100644 index 9614d729..00000000 --- a/pkg/pmapi/paging.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -const defaultPageSize = 100 - -func doPaged(elements []string, pageSize int, fn func([]string) error) error { //nolint:unparam - for len(elements) > pageSize { - if err := fn(elements[:pageSize]); err != nil { - return err - } - - elements = elements[pageSize:] - } - - return fn(elements) -} diff --git a/pkg/pmapi/passwords.go b/pkg/pmapi/passwords.go deleted file mode 100644 index f03628ad..00000000 --- a/pkg/pmapi/passwords.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "encoding/base64" - - "github.com/ProtonMail/go-srp" - "github.com/pkg/errors" -) - -// HashMailboxPassword expectects 128bit long salt encoded by standard base64. -func HashMailboxPassword(password []byte, salt string) ([]byte, error) { - if salt == "" { - return password, nil - } - - decodedSalt, err := base64.StdEncoding.DecodeString(salt) - if err != nil { - return nil, errors.Wrap(err, "failed to decode salt") - } - - hash, err := srp.MailboxPassword(password, decodedSalt) - if err != nil { - return nil, errors.Wrap(err, "failed to hash password") - } - - return hash[len(hash)-31:], nil -} diff --git a/pkg/pmapi/passwords_test.go b/pkg/pmapi/passwords_test.go deleted file mode 100644 index 6c9440d2..00000000 --- a/pkg/pmapi/passwords_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestMailboxPassword(t *testing.T) { - // wantHash was generated with passprase and salt defined below. It - // should not change when changing implementation of the function. - wantHash := []byte("B5nwpsJQSTJ16ldr64Vdq6oeCCn32Fi") - - // Valid salt is 128bit long (16bytes) - // $echo aaaabbbbccccdddd | base64 - salt := "YWFhYWJiYmJjY2NjZGRkZAo=" - - passphrase := []byte("random") - - r := require.New(t) - _, err := HashMailboxPassword(passphrase, "badsalt") - r.Error(err) - - haveHash, err := HashMailboxPassword(passphrase, salt) - r.NoError(err) - r.Equal(wantHash, haveHash) -} diff --git a/pkg/pmapi/pmapi.go b/pkg/pmapi/pmapi.go deleted file mode 100644 index 52429475..00000000 --- a/pkg/pmapi/pmapi.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "github.com/sirupsen/logrus" -) - -var log = logrus.WithField("pkg", "pmapi") //nolint:gochecknoglobals diff --git a/pkg/pmapi/pmapi_test.go b/pkg/pmapi/pmapi_test.go deleted file mode 100644 index 619aa944..00000000 --- a/pkg/pmapi/pmapi_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "os" - "strings" - - "github.com/ProtonMail/gopenpgp/v2/crypto" -) - -const ( - testMailboxPassword = "apple" - testMailboxPasswordLegacy = "123" -) - -var ( - testPrivateKeyRing *crypto.KeyRing - testPublicKeyRing *crypto.KeyRing -) - -func init() { - testPrivateKey := readTestFile("testPrivateKey", false) - testPublicKey := readTestFile("testPublicKey", false) - - var err error - - privKey, err := crypto.NewKeyFromArmored(testPrivateKey) - if err != nil { - panic(err) - } - - privKeyUnlocked, err := privKey.Unlock([]byte(testMailboxPassword)) - if err != nil { - panic(err) - } - - pubKey, err := crypto.NewKeyFromArmored(testPublicKey) - if err != nil { - panic(err) - } - - if testPrivateKeyRing, err = crypto.NewKeyRing(privKeyUnlocked); err != nil { - panic(err) - } - - if testPublicKeyRing, err = crypto.NewKeyRing(pubKey); err != nil { - panic(err) - } -} - -func readTestFile(name string, trimNewlines bool) string { //nolint:unparam - data, err := os.ReadFile("testdata/" + name) - if err != nil { - panic(err) - } - if trimNewlines { - return strings.TrimRight(string(data), "\n") - } - return string(data) -} diff --git a/pkg/pmapi/response.go b/pkg/pmapi/response.go deleted file mode 100644 index 8f43cbfb..00000000 --- a/pkg/pmapi/response.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "math/rand" - "net/http" - "strconv" - "time" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/go-resty/resty/v2" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const ( - errCodeUpgradeApplication = 5003 - errCodePasswordWrong = 8002 - errCodeAuthPaidPlanRequired = 10004 -) - -type Error struct { - Code int - Message string `json:"Error"` -} - -func (err Error) Error() string { - return err.Message -} - -func (m *manager) catchAPIError(_ *resty.Client, res *resty.Response) error { - if !res.IsError() { - return nil - } - - if res.StatusCode() == http.StatusUnauthorized { - return ErrUnauthorized - } - - var err error - - if apiErr, ok := res.Error().(*Error); ok { - switch { - case apiErr.Code == errCodeUpgradeApplication: - if m.cfg.UpgradeApplicationHandler != nil { - m.cfg.UpgradeApplicationHandler() - } - return ErrUpgradeApplication - case apiErr.Code == errCodePasswordWrong: - return ErrPasswordWrong - case apiErr.Code == errCodeAuthPaidPlanRequired: - return ErrPaidPlanRequired - default: - err = apiErr - } - } else { - err = errors.New(res.Status()) - } - - switch res.StatusCode() { - case http.StatusUnprocessableEntity: - err = ErrUnprocessableEntity{err} - case http.StatusBadRequest: - err = ErrBadRequest{err} - } - - return err -} - -func updateTime(_ *resty.Client, res *resty.Response) error { - if date, err := time.Parse(time.RFC1123, res.Header().Get("Date")); err != nil { - log.WithError(err).Warning("Cannot parse header date") - } else { - crypto.UpdateTime(date.Unix()) - } - - return nil -} - -func logConnReuse(_ *resty.Client, res *resty.Response) error { - if !res.Request.TraceInfo().IsConnReused { - logrus.WithField("host", res.Request.URL).Trace("Connection was NOT reused") - } - - return nil -} - -func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error) { - if res.StatusCode() == http.StatusTooManyRequests { - if after := res.Header().Get("Retry-After"); after != "" { - l := log. - WithField("statusCode", res.StatusCode()). - WithField("url", res.Request.URL). - WithField("verb", res.Request.Method) - - seconds, err := strconv.Atoi(after) - if err != nil { - l.WithError(err).Warning("Cannot convert Retry-After to number") - seconds = 10 - } - - // To avoid spikes when all clients retry at the same time, we add some random wait. - seconds += rand.Intn(10) //nolint:gosec // It is OK to use weak random number generator here. - l = l.WithField("seconds", seconds).WithField("start", time.Now().Unix()) - - // Maximum retry time in client is is one minute. But - // here wait times can be longer e.g. high API load - l.Warn("Retrying after induced by http code. Waiting now...") - time.Sleep(time.Duration(seconds) * time.Second) - l.Warn("Wait done") - return 0, nil - } - } - - // 0 and no error means default behaviour which is exponential backoff with jitter. - return 0, nil -} - -func (m *manager) shouldRetry(res *resty.Response, err error) bool { - if isRetryDisabled(res.Request.Context()) { - return false - } - if isTooManyRequest(res) { - return true - } - if isNoResponse(res, err) { - // Even if the context of request allows to retry we should check - // whether the server is reachable or not. In some cases the we can - // keep retrying but also report that connection is lost. - go m.pingUntilSuccess() - return true - } - return false -} - -func isTooManyRequest(res *resty.Response) bool { - return res.StatusCode() == http.StatusTooManyRequests -} - -func isNoResponse(res *resty.Response, err error) bool { - // Do not retry TLS failures - if errors.Is(err, ErrTLSMismatch) { - return false - } - return res.RawResponse == nil && err != nil -} - -func wrapNoConnection(res *resty.Response, err error) (*resty.Response, error) { - if err, ok := err.(*resty.ResponseError); ok { - return res, err - } - - if errors.Is(err, context.Canceled) { - return res, err - } - - if res.RawResponse != nil { - return res, err - } - - // Log useful information and return back nicer and clear error message. - logrus.WithError(err).WithField("url", res.Request.URL).Warn("No internet connection") - return res, ErrNoConnection -} diff --git a/pkg/pmapi/server_test.go b/pkg/pmapi/server_test.go deleted file mode 100644 index e85ff25b..00000000 --- a/pkg/pmapi/server_test.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "regexp" - "runtime" - "strconv" - "testing" - "time" - - "github.com/hashicorp/go-multierror" - r "github.com/stretchr/testify/require" -) - -var ( - colRed = "\033[1;31m" - colNon = "\033[0;39m" - reHTTPCode = regexp.MustCompile(`(HTTP|get|post|put|delete)_(\d{3}).*.json`) -) - -func newTestConfig(url string) Config { - return Config{ - HostURL: url, - AppVersion: "GoPMAPI_1.0.14", - } -} - -// newTestClient is old function and should be replaced everywhere by newTestClientCallbacks. -func newTestClient(h http.Handler) (*httptest.Server, Client) { - s := httptest.NewServer(h) - - return s, newManager(newTestConfig(s.URL)).NewClient(testUID, testAccessToken, testRefreshToken, time.Now().Add(time.Hour)) -} - -func newTestClientCallbacks(tb testing.TB, callbacks ...func(testing.TB, http.ResponseWriter, *http.Request) string) (func(), Client) { - reqNum := 0 - _, file, line, _ := runtime.Caller(1) - file = filepath.Base(file) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - reqNum++ - if reqNum > len(callbacks) { - fmt.Printf( - "%s:%d: %sServer was requested %d times which is more requests than expected %d times%s\n\n", - file, line, colRed, reqNum, len(callbacks), colNon, - ) - tb.FailNow() - } - response := callbacks[reqNum-1](tb, w, r) - if response != "" { - writeJSONResponsefromFile(tb, w, response, reqNum-1) - } - })) - - finish := func() { - server.CloseClientConnections() // Closing without waiting for finishing requests. - if reqNum != len(callbacks) { - fmt.Printf( - "%s:%d: %sServer was requested %d times but expected to be %d times%s\n\n", - file, line, colRed, reqNum, len(callbacks), colNon, - ) - tb.Error("server failed") - } - } - - return finish, newManager(newTestConfig(server.URL)).NewClient(testUID, testAccessToken, testRefreshToken, time.Now().Add(time.Hour)) -} - -func checkMethodAndPath(r *http.Request, method, path string) error { - var result *multierror.Error - if err := checkHeader(r.Header, "x-pm-appversion", "GoPMAPI_1.0.14"); err != nil { - result = multierror.Append(result, err) - } - if r.Method != method { - err := fmt.Errorf("Invalid request method expected %v, got %v", method, r.Method) - result = multierror.Append(result, err) - } - if r.URL.RequestURI() != path { - err := fmt.Errorf("Invalid request path expected %v, got %v", path, r.URL.RequestURI()) - result = multierror.Append(result, err) - } - return result.ErrorOrNil() -} - -func writeJSONResponsefromFile(tb testing.TB, w http.ResponseWriter, response string, reqNum int) { - if match := reHTTPCode.FindAllSubmatch([]byte(response), -1); len(match) != 0 { - httpCode, err := strconv.Atoi(string(match[0][len(match[0])-1])) - r.NoError(tb, err) - w.WriteHeader(httpCode) - } - f, err := os.Open("./testdata/routes/" + response) - r.NoError(tb, err) - w.Header().Set("content-type", "application/json;charset=utf-8") - w.Header().Set("x-test-pmapi-response", fmt.Sprintf("%s:%d", tb.Name(), reqNum)) - _, err = io.Copy(w, f) - r.NoError(tb, err) -} - -func checkHeader(h http.Header, field, exp string) error { - val := h.Get(field) - if val != exp { - msg := "wrong field %s expected %q but have %q" - return fmt.Errorf(msg, field, exp, val) - } - return nil -} - -func isAuthReq(r *http.Request, uid, token string) error { //nolint:unparam always retrieves testUID - if err := checkHeader(r.Header, "x-pm-uid", uid); err != nil { - return err - } - if err := checkHeader(r.Header, "authorization", "Bearer "+token); err != nil { //nolint:revive can return the error right away but this is easier to read - return err - } - return nil -} diff --git a/pkg/pmapi/settings.go b/pkg/pmapi/settings.go deleted file mode 100644 index e5c71e63..00000000 --- a/pkg/pmapi/settings.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - - "github.com/go-resty/resty/v2" -) - -type MailSettings struct { - DisplayName string - Signature string `json:",omitempty"` - Theme string `json:",omitempty"` - AutoSaveContacts int - AutoWildcardSearch int - ComposerMode int - MessageButtons int - ShowImages int - ShowMoved int - ViewMode int - ViewLayout int - SwipeLeft int - SwipeRight int - AlsoArchive int - Hotkeys int - PMSignature int - ImageProxy int - TLS int - RightToLeft int - AttachPublicKey int - Sign int - PGPScheme PackageFlag - PromptPin int - Autocrypt int - NumMessagePerPage int - DraftMIMEType string - ReceiveMIMEType string - ShowMIMEType string - - // Undocumented -- there's only `null` in example: - // AutoResponder string -} - -// GetMailSettings gets contact details specified by contact ID. -func (c *client) GetMailSettings(ctx context.Context) (settings MailSettings, err error) { - var res struct { - MailSettings MailSettings - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&res).Get("/mail/v4/settings") - }); err != nil { - return MailSettings{}, err - } - - return res.MailSettings, nil -} diff --git a/pkg/pmapi/testdata/keyring_addressKeysPrimaryHasToken_JSON b/pkg/pmapi/testdata/keyring_addressKeysPrimaryHasToken_JSON deleted file mode 100644 index 321e0b9a..00000000 --- a/pkg/pmapi/testdata/keyring_addressKeysPrimaryHasToken_JSON +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "ID": "hKRtZeTDhvzfAaycb5BOVx6Y3hc3gs4QvET8H_YZBTwAQBPp3h6FI4nnkJePCYuM9CG0zf7TQzOJeB2rPi0YmQ==", - "Primary": 1, - "Active": 1, - "Flags": 3, - "Version": 3, - "Activation": null, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nxcMGBFzGzj4BCADh9uuhG3mDhlbFMgQpvP6AReyJYa6KSV9a6AJsNI0/DkVz\nFenIodav/r4RBgSoblqxCa/QH6galbX0eB8Vtxx4xTXCcHHZzTM9oAb4s8r6\ndGchNsSkoLspVwmc2fYfDRCTcapmeza7Jym8gD9wFUSWSwpC49jDeT6VIcOd\n+qNXgIJX1V36BOAB53lKenMYnxfzzAyJDeL/sQ/SWE9h/X+wGdkVmk7gk3pa\nQ9URoxAf7MO808hHyQYcjTV4tOHD/9k+AZC6sqmr2KAefYCxCMoCDj1wQp3J\nN6MfK2Zvc1r5KFQM2YzUXrMNrycAjo7gGeZnsvz758Q+8ouSP7iXGRl/ABEB\nAAH+CQMIQt3QQIJZYjRgTVgIQNSEkhMn4GJ5lQQPBXD0/YyK3xP0bm/bsX8S\nz9dAm1nkP0l8Q9z3lR6OcAX2O/KsqJdQGmJEZAaadz2je+EEWo1FDYaL76E6\n6z+AuteP8UtP61jBt3pTfIlhW8o7o2SlM1lvytpvc5FQplZ/iUuwfsIltCiD\n9VDkprNOVrRsjoh8BV4TrrIvU8qga58Aeg7SjOmm+3oMZ7yPTYirr11Tx/m3\nj1uzdEDfiPk4LmvlzSsWwKZuy6fSul+n92+9qN81wmdts/I2ucuKvOINZim4\nlk/p2AOsPjWpAgkefpTVLZnu2IH+VAyaXt1Fl84badXx4N921nPs7ova1Ud5\n2RddBc7b/01DtOyBSWDoNskLGpsc2mqz9kdkwwQKNjChzZc9nmY9M+AIfgT3\n+2DSQIuoJYPX69DKi/bZDwRzoHmiwiHT6Us7qxd6kD1dzCIHTptxwZQp4Tow\nnN6lmtK4S6O2B47+ROn8s0N+EH9GR8F6mvOTayNLH5yicpR3M4Of0ClvFa0G\n+JUKBXIQXUvF03G3nTPU17nLibC81UmbK3zobfbrLfuU2gU+sY+OfE1E/+GO\nSFpZcrkoRqRr39CfLkLk+GjU7RCLNddb5LxgaurVZo5h0Y7Rr8VvOQMWjjl/\nvTAG7gU/HtXi24TijNC0fP6j9w43K4b6t1SZYn5us7RRlFlWGKMSPf5Q9j6/\nryo3xULUQv0lTCQPtfQQ5UWE5ZpQ4Kjt/k5+/YtfuOcMbrDU4qa+H+rrisBu\nko7f4Wn0iYjRwRIuWh1NfUM3rIbNhq7/wonasEFOeFdPwprzMaawx3rL0Pq5\nGs/4LONqG61c9rBekbkGf7Jlkuq/5yo5RBgPnKwvJKsHqf1evD6kHC3aOfeO\n30UoMwe7g763pOXZrsOpZfPzxmraJJSYzS0iYXJvbjIxLTRAc2FkZW1iZS5v\ncmciIDxhcm9uMjEtNEBzYWRlbWJlLm9yZz7CwHUEEAEIAB8FAlzGzj4GCwkH\nCAMCBBUICgIDFgIBAhkBAhsDAh4BAAoJEC3c5LbRFpy6lOgH/RKA4QaTnCi8\nc7HHhDZncqwBSUNhTjCAvoiireX9gGm9JugxaPxHVH4RzznY6R7/Ui8Ak7S8\n+k/xhHbsOGc2exyWwUN1X3WJY3jSX2HNqDU4qw6hSwBGReYbOWJeKGhJWild\nPS4V6u6manGWXxQmW1XET3B0P72VJVSX1kUbslPBAhKbW3JCnDmEdV/sU6Ds\nXdh543Yph1OpO2Fq7b/+YYUDAzmHf/+k4ijVcvqjrjUCJJwv+2J9woi4ToW6\n3BQVG5gpAYzCfgoJjlaigInhoFrBjP25Oe6/ssDTssGJrHXhtyc8e+b7nm19\nSHOpWGcUn2F1+tU+E4O8SLCLGJxefJvHwwYEXMbOPgEIAKEkpHRBhTWOIeYB\ngPXh/Ng77x3Bs6EKwTQM/BePYC2uS+15+nIpiYHhb/sQ9aEbQgqmyfbkbfIf\n9Qahx4N9RFyqWcmSjfk0Bmo9xOziRQm1tfYkbUwkeI6NIr2ENUWVf7tt+UKz\n5dFmvSKsyrdPEtt+Ken17JoihhJ/9saLMkLn5Y3HrSGVXIniX1cQuarXGX9S\nyt7jIPeGZW2suuxlnlB6Sa/rCkaqR3C3a8knxiH8CDwAm+E8a1d/UbQ0np0k\nqTVVrc2fmxPMgZxGWrwIsO9D1Fs7dgw5rac7ijHvPXeWrzbMd1+rX5mmLF4+\nH2PDOUKN0Uu8WdVilCIEOMoVfY0AEQEAAf4JAwiPkDWd2zmFhmBFMZgWR+X2\nhS4q8bIohwFb8MDzrHAvtI8LOaxC32j3/tUwJKDwLNDeiiOGYfHUOmqgzVRi\nMBdkoVNT3OyFLbw6k/70spe5OcecZsM+OAQybX77Kv3H/VW/40TtUh52Zvvk\nqCoqtG86C4R5kma0zM+yvNyprkCIRwYuJ+OkdmMvem3fRqU49GNzJChBDAmZ\nfGc7W7r2JFIo1k88bh+kaGlDBA9p8GnA7KNXAlBdq6owJNJA0j2z6Rmngpip\nFuUEjdW8Kcs2ben4BZOWGgYmOmD1CAHRYm/xp+Us1kJDTTFxW7P7BNp1kVOP\n9s9AcV/t37bCHLVB2IjAb/tkOxyAexMTS/lFJrpJ89MpYiUt70uB+SDwRls9\noud9CQYS+6OdddgFUjtnOkpcR1Y24v1eF13JwUXmnPggvt6Do3gS6lGdq0Nt\nqNDg8+yPyMA8365Q8IbJDuzm3vfc560Szc/Kx4+1zr57Uaw/qYqhZHSkjRtZ\nfTP0v0ZNwxyXhGF3J31neJ5KgIO+zpSWSP8RwtUVBb8Tsyn8e/DSe/8fAs3V\ntmyxSj9mPQLxW8JCQMpocExEJV7PnxjhB0d1TlYu/wRUl4sxpS4pYa/TDhb9\n37oUYtG28TufT7AZE+XcU8Y1Xl7DVi5jgadUAI5rY71G31JeBuIcGRlVLBnT\nSHnu1v2iz1xqZKQFynH6DUrySn8nP8NM9TaVEnSBlVCjHcyvrs8crx358YFV\n4TG44XQ3n5GLjPetKGD/ccMYOUZm2jkLNzY9l4YqKJq7xsm2c5VhwUEB51Pi\n8Ey+x7hO3EjWUgJrtI/3/hPoKlltPUvnhJjNrR8ivocvd2v+2U0BHQOwTUt3\njSDssFDI9hfOMPp4yy/GHM+p3USC+3TMS8HjFbm6b2bIWzqEv83AaauZqiE1\nfJfCwF8EGAEIAAkFAlzGzj4CGwwACgkQLdzkttEWnLqI8Af/Uimov/qMfFdi\nSp1qYTzR9V84ZkfxBAcr9nEyNGtMlvCwrs3EtZPBe7ou57Qt9aI8UjJfsiov\naHUC1Gt8266T1+GGj6RCPSm6Sp7cZxGtURZDWAPb5u02+VtrqrrXQgkCxQ8/\nzWfg4vLyl052x+3F9SGy7SvH0qi8bcHzVjcwK9VyoSdBq/vhkPHQ33wLQ9ND\nuQCM8fxW+VOqMDlUCHfdcaYRYU7GEm9C37ZijpOLZRuAm6ojjCGtrOrGQ+y0\ni3q7Zn1yDradMlMGG9GjDlqOCmYhZbuT4uS578GzPg0L8zk/1rFOF+YY6Mfq\nV0GB3IX8qBHjAfPmqN9JPxBIn3/6ag==\n=U1yD\n-----END PGP PRIVATE KEY BLOCK-----\n", - "Token": "-----BEGIN PGP MESSAGE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwcBMAxW/uQhm1PKxAQf8D4BAoXqnYgoTr7jEiabP9NkeuD6Z88fZDgUzXte5\n6aYMmCtPr0Vef1eDl2x0S0q/YJgR+A5Icmjxk+jG9nMSnRPlAozJfTCXu/Oy\nfkI4CcClGQv7U1EzfknBVNEKmuPO8XlEkGZ5lW/TzWk8ZtQqJsVuqZlZ5qQE\n/p8FNYEZ5RHPhhKlotA+T6XKh23z9mcN/JNEsSNcx+guERbivtTwnd5JpxQJ\nMNWzUYh+K0W7LgxoAo2jevDd4CZ/0sAAViIl+QrShodseV9agbldGUUoIMzn\nHUuIV6VYi7X82eKQXDInrtPc9IHekbDDFoncLnsGGrEqD/8O/qOHHOPwNwU+\n+cHATAOUTDv/ccWq1QEIALl8SNOQBmCuanAceUTSwCkM1fC3Ddqa8ZmnMMyG\nnDWDI7XkFU31CO10lN20/kAWjdN2073B/NT+17cp8fCqbQ1pAFJHpabdqmbI\ntm3pCC5M6otTN+MhjgFYcBuxo0rq9qtuEzz5j4Ub9MIIJTurUHMEPMI462Dc\ndK/d8BvDkU7q67Lkp65vpe9e/pv0lMMrQjdohnTHNgbZhbI/Z5LU0ApD//Ye\ncSC26BRUMJITiGKb9pKGAi0/ig0jJfzykgEarzOsY/v0W7016AMka+NsHuNc\n7Qfyg2LApF5s9Z6aK+Uy851haUk+p+abjBdcWACziAmimVGjlRYm49ra4UJ7\nc6LSeAElM584e4Z3VnqajAJWbmWt2atgBQmcwEBBsdNAtzIagNydMbBBIELA\n5wGUbMXfQxrMOo/Mdac/5EMTHT0/kVQNIBHtpX5SknWvbc5DjIHoH3+BbL87\neaxhKW94hMubKNKT3dbm4PKHtsMiS3TGkFZ3GjvA0Cy/Kw==\n=b1N8\n-----END PGP MESSAGE-----\n", - "Signature": "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwsBcBAEBCAAGBQJcxtSkAAoJEGfweL9m9sQefz8IAJhpEOvG7d+PVgq3bEL+\nXzY8yTUKB1ZXEMbcR/uHUWSfu0F8zy4CNtSG5HUVjNxx1xzzJVJcxWB/ljO+\n/bJSFOFexTyNh8i/xU+CiBfm5RhAFTYF9xFwfD3LKp5gaalJAhWhArk1/Wuh\nWTDpFpk39uzBRKNwcnSgiJYPxOjAZxj+w/hhHPwmco5cUwMiMR5MNrfzKf+x\nwX3Cfs9fsiiCDzohBzbK0FFsMnJ8aXNVsDBjEA1KrB7sdyaf8FnaM0RsFRDb\njxtIgfcFBbyNJh4414Unt4AYTIrIWhK4OOXI3AfsJy8p6KRBKQUcUkKcDxab\nPTXOPZsZ+UgQ5MevyVnP1zfCwFwEAQEIAAYFAlzG1KQACgkQm1DMMBzxetx/\nPwf9F04uHtix0zDpP1IvG4VYlor4rjYTdfXqxxiFXHO6MZXJoigS1E71E8r4\nsqZ6PoQ5/xCj2A01KRhuF1Bon3mEZEwaIUuBqTV91sLsVWfoxgyPAYpr6gK/\n1W9JhVNNrVRMGox42LQUjyiq0ESrfWmqC8SuMfZMoUoBZycHicA50RbyOUnT\nLO57ArL3JIVmYtyosaXM3idzmNHmaXSkcGt4cvTVysJZQrneaxmikfm5CH2O\n1z2goLBNnzsbRionoV6gCukZOiM/d14yiyeYtsFJ7u/vodkI5y0M0sF8VnN/\njL9keP4ZpHiJ4MBd6tyIH0pLueDRlurFL3fcHsEzD5SrOA==\n=wGCk\n-----END PGP SIGNATURE-----\n" - }, - { - "ID": "MhF8dxtN5Lz_GruskN3L9kTKWusZHBdhWaxc7w1tgze2qB6uM9AyC6mWRQg1B7WGZ_r-9gn-XC95-IkpNJr0jg==", - "Primary": 0, - "Active": 1, - "Flags": 3, - "Version": 3, - "Activation": null, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlQPGBF2IcLUBCACl7vans5JKYAEuOZfKNDLZKmLO675DhLjbno8Pw4Q8O6xQETUZ\nTFsIB6eTigCqaZasCeHIx4r7CI4TY2uZvV3+JNaAHN0omHh3iwVXQiE3FU5Zc6TO\niVswOoWPbVYFTuItP3FQHuossMIralIzG0/AyDx0NJawwpSxxz9+C0UoYeZD1A95\nfKveRVhDW0MgO1peDHrpr2HiWv06KkIop39CD/MBnBtDpsarGd16wvX+r8F3MrNE\nD1/e6szzi7oR5H0hPs+HSWZexXhiGSWoEyMiklX/sG/9+LxnHqPhCj7lrGfzmL7j\nCFuHZIqpIO8wVggOQYx1mtmMu7T9PZm66WMbABEBAAH+BwMCYpZ0ni+oe4DxIByk\nseTvdCIjQ3ieiTVvg9FsIVrupmIxDK9EVsGw1/sFjBtc5OOP8N9Vd8IyPh6WjBMu\n5pI5dsYKCLy6LMDmYTYQzXgWlpq2cpH+yZX21fZ+0ijEu+TTSJss3emCULrtCS5M\nUTzt813cIPJu8bYPdQ7taVRqK0Bo+S8YTj2a+lxHQRkbRclueuTm7l5ypU/9X4Xf\n4zwUKnq0OYu3U8IkDEOA+4Zv4SZssFrPr8plUzMHUahVV7NZCXOG11ju1CC9CkF2\n3lpkCZw96TwCGQnT4iTPix1NEJZo6ihRbMlnLo/neS3TWgdCpK5Qr8x4B82ykX6J\ngUeNozW/YFOwFwah22TeSN2dbaGbzJOSqMXHVzK9mz/oTajY/51pS/ZxGoxgy7F2\nD8E/m0I7dS3fw/A/6ZtQFa1bW8L/dBnN71gfr6PSVxn3oczPCXtzNeqHPaDIDGV1\nhgGbGFgmjJy9mnPuxQcScIJloLOVerAaqMcgFuo2MhGKLzQ7IGiSzinaJZ7k602f\nJPfpqBg2aMO9xxNC0S6p6KzMRZCjM1cOJYF2jMo2UWqM0XA1+aEqPksBTmyKSd9+\n1sc8coVl3iesji6GZoT5s0+sNEiQZuNOFUzmGKdgqZhDVfPTLmfV1Uv/tlZCGKmC\ntC51VGjqf8RE3ycdn0Hby8Q2DLsaTnC2vMUde2iJwGQU41TTlt4TKSp6YtqSxbp+\nzUpF4QuS2zVARtx3Z87vw6rHmrNCXHzxl9zWjF3M42FhEESrUL0dciM3c1pB5twd\nIx1TSI0+96zneGS4AkN5Tqua+RdvWXX8ljRgLFa8DqraumQyOjyMXRlJoiey1dt3\n3sRfeF8eI2QF9DooI+4Za/C/1VaEURSQ4p04Ng/xJJ9mwpxjKvxmujC9n2yZ1u4o\nvQh9cKQ9nQAItDdqYW1lcy10ZXN0QHByb3Rvbm1haWwuYmx1ZSA8amFtZXMtdGVz\ndEBwcm90b25tYWlsLmJsdWU+iQE1BBABCAAfBQJdiHC1BgsJBwgDAgQVCAoCAxYC\nAQIZAQIbAwIeAQAKCRC/qkLbur13GVuWB/410LkZxZltVA1odI+yL3utIhoNWj8R\n3AhlVPLfAymkJOFv2KMZ2pRkS8PTmq4sYjAlti60cpWwrNdU4eSfNxoWDEg2lILW\nFULm3TwygDFogSsqyKlGXGr0MqnegImDeQg2kMLPvudTNvEbnRDpaCxWUHenEH5+\nTb8+UyG1e3V2su5J6p+Ghh88Y+zJyU4DWta3wAymLPW+gUEFXDAw5KdSg/8iD5kH\n6aMbd9l1Wp5WiErMsJOwoxhFVhwUK7MCXAXxHMWTuyu78RYYNhafPpd+Rq6dqy1d\nm5M7BlDU4Y2Y921xOSHGbNXbWbh5WHBT0QkV7IJunwiPYc5apJbee69DnQPGBF2I\ncLUBCAC9RsknEX9jPP4AB2/DxWdqTKb1/AmDpzNstEUtSy+zUKmErSNQeNUuso+n\nH2lFtb/A+s3nC2UPqbgz/SWN37Z9ip6hvsQX0etFvzYiXFfVDbknqmpi8AGt1Bkr\nFP9jGvF7gIT436icfjd8vQHHMIdEbEF+S46LfW8xibZEYUHLr5m8VO7dqqsIIwvr\nTJWEGjtRfaacqV3aWDzHbXodmYC3kQH9UjpdXE/9/5VxWtxDAFwsCaXiE9+EL69Z\ncJDdPfM0iVL3XgP22EFwC8C+Xha1PiSNOGFTIbv4aT5yLgPb7wwvgaw3semLF1vM\nzni3z+FNPREt+9itIhGPpxmIyAQ/ABEBAAH+BwMCl4RZZJs4k1bxqOmzniBxB2y8\nX4sDxyy60IOO3Ip2cOeyJ/qwEBdXxL2EjS6y5Vt3GTnQDz1IJqYiz9eRbZQnXUh5\nSk9ND9+mgxKYHDV52flh2FBTaOgqsKLnwLRUE12D1BQ9Ou5cYcgOIbCncftQTnWV\nk6hg9PDFZD/LD3IuKS9/DVYxKkYIJZ1KH446bQTYto+fnU0ajkLfED7wN4JquHq4\nYS+L7h8GetOCyRsh8zDIOPF5jZYCSzDe8Q03ktozOE2+YV/A3Z/0Uub8/Qecl5u3\n1qURXeIC6oGRDVXegZu3491IG+UTzjaBDPu+Xk4zad7mGjBQ9vNuKZfNNg/nHOWL\nNHRCXbkQVpXCA+v+dNYov+bqKe1QpF/2jvU0Bi32HLG8gfmRwD0WaAnbvOLTYXPl\ndRNDn7ixfOC0uHT6epPlZxsRja5TXlFl5qSuAX2zQfFdb+pRPztNKer6iMNz3itz\nh2B/gcTwqXloy4Zq9wqxcXLV1qmoMJ5YzRcd4otRM7FhZD0LxBAOANdmPSmRHbf6\nSXyXgvK4KDwRB52uIYJzWyIi7LiU3qTbV0GgptgHeVLwgbA9ioPwIu85+XQEh/eE\nEjNhz0CdtCqmYY3+SjNOZ8K59LgWCuv/YkRkmwDimEz5gUlpayoOYib4J4wIx6OY\n+fmI+gByS+UN3v+ml7l909c2W8w0dwR8+7P1XGBkaiVROTM2hdnHMflWm2548WBx\na6tbdCQPUDra/5O/pCdefc44hwD08liutq9kF29sMwONZy0P0dlyFZq4LTLmQqs0\nq93GZRjoI+uit9oTLGCtJ99aiSIx97KfkKx5wLnE4DruQlqPWnxV5Yfn9RZ6j8V2\nRQjL8f5RbiBtkkCl3r+nEdQIeMRy+8xxO9dTYTlaCNgfd92LdQrwzKe+GneUgKYg\nWo+AiQEfBBgBCAAJBQJdiHC1AhsMAAoJEL+qQtu6vXcZskIH+we2sHxPnWOfkXTu\npmZBU1ba8JLcrx+CMwTPkt7GMYpDplVOybn+AjMxB9JxST71mBw6rbwd2XLM1AmJ\nbvXVfmCtmC2TmrORqcoxEql+9tdgnSTfZfrltnYeVuEAvAxrMmD4S2Mb5zVWFl+q\ni69A0rdD8a36nOlJnZxfs57W1zfvl/rh+/RdybPwx9y0hSnRPSypis2dcwSyBD9+\nnlbn3QoybaUJxWvL+9MphCXZ4CuuhG0VPcmdH+LzOytDTtJnDNm+Ru4sokroDJXe\n5XlrK3+wtSNL4rfa/MQYyK8XsWRVR/a0BPeqZA4cPlHqCOSfUfaFbORVbPaQdq2a\nYYsMDlA=\n=ZwJf\n-----END PGP PRIVATE KEY BLOCK-----", - "Token": null, - "Signature": null - } -] diff --git a/pkg/pmapi/testdata/keyring_addressKeysSecondaryHasToken_JSON b/pkg/pmapi/testdata/keyring_addressKeysSecondaryHasToken_JSON deleted file mode 100644 index 32a6661a..00000000 --- a/pkg/pmapi/testdata/keyring_addressKeysSecondaryHasToken_JSON +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "ID": "MhF8dxtN5Lz_GruskN3L9kTKWusZHBdhWaxc7w1tgze2qB6uM9AyC6mWRQg1B7WGZ_r-9gn-XC95-IkpNJr0jg==", - "Primary": 1, - "Active": 1, - "Flags": 3, - "Version": 3, - "Activation": null, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlQPGBF2IcLUBCACl7vans5JKYAEuOZfKNDLZKmLO675DhLjbno8Pw4Q8O6xQETUZ\nTFsIB6eTigCqaZasCeHIx4r7CI4TY2uZvV3+JNaAHN0omHh3iwVXQiE3FU5Zc6TO\niVswOoWPbVYFTuItP3FQHuossMIralIzG0/AyDx0NJawwpSxxz9+C0UoYeZD1A95\nfKveRVhDW0MgO1peDHrpr2HiWv06KkIop39CD/MBnBtDpsarGd16wvX+r8F3MrNE\nD1/e6szzi7oR5H0hPs+HSWZexXhiGSWoEyMiklX/sG/9+LxnHqPhCj7lrGfzmL7j\nCFuHZIqpIO8wVggOQYx1mtmMu7T9PZm66WMbABEBAAH+BwMCYpZ0ni+oe4DxIByk\nseTvdCIjQ3ieiTVvg9FsIVrupmIxDK9EVsGw1/sFjBtc5OOP8N9Vd8IyPh6WjBMu\n5pI5dsYKCLy6LMDmYTYQzXgWlpq2cpH+yZX21fZ+0ijEu+TTSJss3emCULrtCS5M\nUTzt813cIPJu8bYPdQ7taVRqK0Bo+S8YTj2a+lxHQRkbRclueuTm7l5ypU/9X4Xf\n4zwUKnq0OYu3U8IkDEOA+4Zv4SZssFrPr8plUzMHUahVV7NZCXOG11ju1CC9CkF2\n3lpkCZw96TwCGQnT4iTPix1NEJZo6ihRbMlnLo/neS3TWgdCpK5Qr8x4B82ykX6J\ngUeNozW/YFOwFwah22TeSN2dbaGbzJOSqMXHVzK9mz/oTajY/51pS/ZxGoxgy7F2\nD8E/m0I7dS3fw/A/6ZtQFa1bW8L/dBnN71gfr6PSVxn3oczPCXtzNeqHPaDIDGV1\nhgGbGFgmjJy9mnPuxQcScIJloLOVerAaqMcgFuo2MhGKLzQ7IGiSzinaJZ7k602f\nJPfpqBg2aMO9xxNC0S6p6KzMRZCjM1cOJYF2jMo2UWqM0XA1+aEqPksBTmyKSd9+\n1sc8coVl3iesji6GZoT5s0+sNEiQZuNOFUzmGKdgqZhDVfPTLmfV1Uv/tlZCGKmC\ntC51VGjqf8RE3ycdn0Hby8Q2DLsaTnC2vMUde2iJwGQU41TTlt4TKSp6YtqSxbp+\nzUpF4QuS2zVARtx3Z87vw6rHmrNCXHzxl9zWjF3M42FhEESrUL0dciM3c1pB5twd\nIx1TSI0+96zneGS4AkN5Tqua+RdvWXX8ljRgLFa8DqraumQyOjyMXRlJoiey1dt3\n3sRfeF8eI2QF9DooI+4Za/C/1VaEURSQ4p04Ng/xJJ9mwpxjKvxmujC9n2yZ1u4o\nvQh9cKQ9nQAItDdqYW1lcy10ZXN0QHByb3Rvbm1haWwuYmx1ZSA8amFtZXMtdGVz\ndEBwcm90b25tYWlsLmJsdWU+iQE1BBABCAAfBQJdiHC1BgsJBwgDAgQVCAoCAxYC\nAQIZAQIbAwIeAQAKCRC/qkLbur13GVuWB/410LkZxZltVA1odI+yL3utIhoNWj8R\n3AhlVPLfAymkJOFv2KMZ2pRkS8PTmq4sYjAlti60cpWwrNdU4eSfNxoWDEg2lILW\nFULm3TwygDFogSsqyKlGXGr0MqnegImDeQg2kMLPvudTNvEbnRDpaCxWUHenEH5+\nTb8+UyG1e3V2su5J6p+Ghh88Y+zJyU4DWta3wAymLPW+gUEFXDAw5KdSg/8iD5kH\n6aMbd9l1Wp5WiErMsJOwoxhFVhwUK7MCXAXxHMWTuyu78RYYNhafPpd+Rq6dqy1d\nm5M7BlDU4Y2Y921xOSHGbNXbWbh5WHBT0QkV7IJunwiPYc5apJbee69DnQPGBF2I\ncLUBCAC9RsknEX9jPP4AB2/DxWdqTKb1/AmDpzNstEUtSy+zUKmErSNQeNUuso+n\nH2lFtb/A+s3nC2UPqbgz/SWN37Z9ip6hvsQX0etFvzYiXFfVDbknqmpi8AGt1Bkr\nFP9jGvF7gIT436icfjd8vQHHMIdEbEF+S46LfW8xibZEYUHLr5m8VO7dqqsIIwvr\nTJWEGjtRfaacqV3aWDzHbXodmYC3kQH9UjpdXE/9/5VxWtxDAFwsCaXiE9+EL69Z\ncJDdPfM0iVL3XgP22EFwC8C+Xha1PiSNOGFTIbv4aT5yLgPb7wwvgaw3semLF1vM\nzni3z+FNPREt+9itIhGPpxmIyAQ/ABEBAAH+BwMCl4RZZJs4k1bxqOmzniBxB2y8\nX4sDxyy60IOO3Ip2cOeyJ/qwEBdXxL2EjS6y5Vt3GTnQDz1IJqYiz9eRbZQnXUh5\nSk9ND9+mgxKYHDV52flh2FBTaOgqsKLnwLRUE12D1BQ9Ou5cYcgOIbCncftQTnWV\nk6hg9PDFZD/LD3IuKS9/DVYxKkYIJZ1KH446bQTYto+fnU0ajkLfED7wN4JquHq4\nYS+L7h8GetOCyRsh8zDIOPF5jZYCSzDe8Q03ktozOE2+YV/A3Z/0Uub8/Qecl5u3\n1qURXeIC6oGRDVXegZu3491IG+UTzjaBDPu+Xk4zad7mGjBQ9vNuKZfNNg/nHOWL\nNHRCXbkQVpXCA+v+dNYov+bqKe1QpF/2jvU0Bi32HLG8gfmRwD0WaAnbvOLTYXPl\ndRNDn7ixfOC0uHT6epPlZxsRja5TXlFl5qSuAX2zQfFdb+pRPztNKer6iMNz3itz\nh2B/gcTwqXloy4Zq9wqxcXLV1qmoMJ5YzRcd4otRM7FhZD0LxBAOANdmPSmRHbf6\nSXyXgvK4KDwRB52uIYJzWyIi7LiU3qTbV0GgptgHeVLwgbA9ioPwIu85+XQEh/eE\nEjNhz0CdtCqmYY3+SjNOZ8K59LgWCuv/YkRkmwDimEz5gUlpayoOYib4J4wIx6OY\n+fmI+gByS+UN3v+ml7l909c2W8w0dwR8+7P1XGBkaiVROTM2hdnHMflWm2548WBx\na6tbdCQPUDra/5O/pCdefc44hwD08liutq9kF29sMwONZy0P0dlyFZq4LTLmQqs0\nq93GZRjoI+uit9oTLGCtJ99aiSIx97KfkKx5wLnE4DruQlqPWnxV5Yfn9RZ6j8V2\nRQjL8f5RbiBtkkCl3r+nEdQIeMRy+8xxO9dTYTlaCNgfd92LdQrwzKe+GneUgKYg\nWo+AiQEfBBgBCAAJBQJdiHC1AhsMAAoJEL+qQtu6vXcZskIH+we2sHxPnWOfkXTu\npmZBU1ba8JLcrx+CMwTPkt7GMYpDplVOybn+AjMxB9JxST71mBw6rbwd2XLM1AmJ\nbvXVfmCtmC2TmrORqcoxEql+9tdgnSTfZfrltnYeVuEAvAxrMmD4S2Mb5zVWFl+q\ni69A0rdD8a36nOlJnZxfs57W1zfvl/rh+/RdybPwx9y0hSnRPSypis2dcwSyBD9+\nnlbn3QoybaUJxWvL+9MphCXZ4CuuhG0VPcmdH+LzOytDTtJnDNm+Ru4sokroDJXe\n5XlrK3+wtSNL4rfa/MQYyK8XsWRVR/a0BPeqZA4cPlHqCOSfUfaFbORVbPaQdq2a\nYYsMDlA=\n=ZwJf\n-----END PGP PRIVATE KEY BLOCK-----", - "Token": null, - "Signature": null - }, - { - "ID": "hKRtZeTDhvzfAaycb5BOVx6Y3hc3gs4QvET8H_YZBTwAQBPp3h6FI4nnkJePCYuM9CG0zf7TQzOJeB2rPi0YmQ==", - "Primary": 0, - "Active": 1, - "Flags": 3, - "Version": 3, - "Activation": null, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nxcMGBFzGzj4BCADh9uuhG3mDhlbFMgQpvP6AReyJYa6KSV9a6AJsNI0/DkVz\nFenIodav/r4RBgSoblqxCa/QH6galbX0eB8Vtxx4xTXCcHHZzTM9oAb4s8r6\ndGchNsSkoLspVwmc2fYfDRCTcapmeza7Jym8gD9wFUSWSwpC49jDeT6VIcOd\n+qNXgIJX1V36BOAB53lKenMYnxfzzAyJDeL/sQ/SWE9h/X+wGdkVmk7gk3pa\nQ9URoxAf7MO808hHyQYcjTV4tOHD/9k+AZC6sqmr2KAefYCxCMoCDj1wQp3J\nN6MfK2Zvc1r5KFQM2YzUXrMNrycAjo7gGeZnsvz758Q+8ouSP7iXGRl/ABEB\nAAH+CQMIQt3QQIJZYjRgTVgIQNSEkhMn4GJ5lQQPBXD0/YyK3xP0bm/bsX8S\nz9dAm1nkP0l8Q9z3lR6OcAX2O/KsqJdQGmJEZAaadz2je+EEWo1FDYaL76E6\n6z+AuteP8UtP61jBt3pTfIlhW8o7o2SlM1lvytpvc5FQplZ/iUuwfsIltCiD\n9VDkprNOVrRsjoh8BV4TrrIvU8qga58Aeg7SjOmm+3oMZ7yPTYirr11Tx/m3\nj1uzdEDfiPk4LmvlzSsWwKZuy6fSul+n92+9qN81wmdts/I2ucuKvOINZim4\nlk/p2AOsPjWpAgkefpTVLZnu2IH+VAyaXt1Fl84badXx4N921nPs7ova1Ud5\n2RddBc7b/01DtOyBSWDoNskLGpsc2mqz9kdkwwQKNjChzZc9nmY9M+AIfgT3\n+2DSQIuoJYPX69DKi/bZDwRzoHmiwiHT6Us7qxd6kD1dzCIHTptxwZQp4Tow\nnN6lmtK4S6O2B47+ROn8s0N+EH9GR8F6mvOTayNLH5yicpR3M4Of0ClvFa0G\n+JUKBXIQXUvF03G3nTPU17nLibC81UmbK3zobfbrLfuU2gU+sY+OfE1E/+GO\nSFpZcrkoRqRr39CfLkLk+GjU7RCLNddb5LxgaurVZo5h0Y7Rr8VvOQMWjjl/\nvTAG7gU/HtXi24TijNC0fP6j9w43K4b6t1SZYn5us7RRlFlWGKMSPf5Q9j6/\nryo3xULUQv0lTCQPtfQQ5UWE5ZpQ4Kjt/k5+/YtfuOcMbrDU4qa+H+rrisBu\nko7f4Wn0iYjRwRIuWh1NfUM3rIbNhq7/wonasEFOeFdPwprzMaawx3rL0Pq5\nGs/4LONqG61c9rBekbkGf7Jlkuq/5yo5RBgPnKwvJKsHqf1evD6kHC3aOfeO\n30UoMwe7g763pOXZrsOpZfPzxmraJJSYzS0iYXJvbjIxLTRAc2FkZW1iZS5v\ncmciIDxhcm9uMjEtNEBzYWRlbWJlLm9yZz7CwHUEEAEIAB8FAlzGzj4GCwkH\nCAMCBBUICgIDFgIBAhkBAhsDAh4BAAoJEC3c5LbRFpy6lOgH/RKA4QaTnCi8\nc7HHhDZncqwBSUNhTjCAvoiireX9gGm9JugxaPxHVH4RzznY6R7/Ui8Ak7S8\n+k/xhHbsOGc2exyWwUN1X3WJY3jSX2HNqDU4qw6hSwBGReYbOWJeKGhJWild\nPS4V6u6manGWXxQmW1XET3B0P72VJVSX1kUbslPBAhKbW3JCnDmEdV/sU6Ds\nXdh543Yph1OpO2Fq7b/+YYUDAzmHf/+k4ijVcvqjrjUCJJwv+2J9woi4ToW6\n3BQVG5gpAYzCfgoJjlaigInhoFrBjP25Oe6/ssDTssGJrHXhtyc8e+b7nm19\nSHOpWGcUn2F1+tU+E4O8SLCLGJxefJvHwwYEXMbOPgEIAKEkpHRBhTWOIeYB\ngPXh/Ng77x3Bs6EKwTQM/BePYC2uS+15+nIpiYHhb/sQ9aEbQgqmyfbkbfIf\n9Qahx4N9RFyqWcmSjfk0Bmo9xOziRQm1tfYkbUwkeI6NIr2ENUWVf7tt+UKz\n5dFmvSKsyrdPEtt+Ken17JoihhJ/9saLMkLn5Y3HrSGVXIniX1cQuarXGX9S\nyt7jIPeGZW2suuxlnlB6Sa/rCkaqR3C3a8knxiH8CDwAm+E8a1d/UbQ0np0k\nqTVVrc2fmxPMgZxGWrwIsO9D1Fs7dgw5rac7ijHvPXeWrzbMd1+rX5mmLF4+\nH2PDOUKN0Uu8WdVilCIEOMoVfY0AEQEAAf4JAwiPkDWd2zmFhmBFMZgWR+X2\nhS4q8bIohwFb8MDzrHAvtI8LOaxC32j3/tUwJKDwLNDeiiOGYfHUOmqgzVRi\nMBdkoVNT3OyFLbw6k/70spe5OcecZsM+OAQybX77Kv3H/VW/40TtUh52Zvvk\nqCoqtG86C4R5kma0zM+yvNyprkCIRwYuJ+OkdmMvem3fRqU49GNzJChBDAmZ\nfGc7W7r2JFIo1k88bh+kaGlDBA9p8GnA7KNXAlBdq6owJNJA0j2z6Rmngpip\nFuUEjdW8Kcs2ben4BZOWGgYmOmD1CAHRYm/xp+Us1kJDTTFxW7P7BNp1kVOP\n9s9AcV/t37bCHLVB2IjAb/tkOxyAexMTS/lFJrpJ89MpYiUt70uB+SDwRls9\noud9CQYS+6OdddgFUjtnOkpcR1Y24v1eF13JwUXmnPggvt6Do3gS6lGdq0Nt\nqNDg8+yPyMA8365Q8IbJDuzm3vfc560Szc/Kx4+1zr57Uaw/qYqhZHSkjRtZ\nfTP0v0ZNwxyXhGF3J31neJ5KgIO+zpSWSP8RwtUVBb8Tsyn8e/DSe/8fAs3V\ntmyxSj9mPQLxW8JCQMpocExEJV7PnxjhB0d1TlYu/wRUl4sxpS4pYa/TDhb9\n37oUYtG28TufT7AZE+XcU8Y1Xl7DVi5jgadUAI5rY71G31JeBuIcGRlVLBnT\nSHnu1v2iz1xqZKQFynH6DUrySn8nP8NM9TaVEnSBlVCjHcyvrs8crx358YFV\n4TG44XQ3n5GLjPetKGD/ccMYOUZm2jkLNzY9l4YqKJq7xsm2c5VhwUEB51Pi\n8Ey+x7hO3EjWUgJrtI/3/hPoKlltPUvnhJjNrR8ivocvd2v+2U0BHQOwTUt3\njSDssFDI9hfOMPp4yy/GHM+p3USC+3TMS8HjFbm6b2bIWzqEv83AaauZqiE1\nfJfCwF8EGAEIAAkFAlzGzj4CGwwACgkQLdzkttEWnLqI8Af/Uimov/qMfFdi\nSp1qYTzR9V84ZkfxBAcr9nEyNGtMlvCwrs3EtZPBe7ou57Qt9aI8UjJfsiov\naHUC1Gt8266T1+GGj6RCPSm6Sp7cZxGtURZDWAPb5u02+VtrqrrXQgkCxQ8/\nzWfg4vLyl052x+3F9SGy7SvH0qi8bcHzVjcwK9VyoSdBq/vhkPHQ33wLQ9ND\nuQCM8fxW+VOqMDlUCHfdcaYRYU7GEm9C37ZijpOLZRuAm6ojjCGtrOrGQ+y0\ni3q7Zn1yDradMlMGG9GjDlqOCmYhZbuT4uS578GzPg0L8zk/1rFOF+YY6Mfq\nV0GB3IX8qBHjAfPmqN9JPxBIn3/6ag==\n=U1yD\n-----END PGP PRIVATE KEY BLOCK-----\n", - "Token": "-----BEGIN PGP MESSAGE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwcBMAxW/uQhm1PKxAQf8D4BAoXqnYgoTr7jEiabP9NkeuD6Z88fZDgUzXte5\n6aYMmCtPr0Vef1eDl2x0S0q/YJgR+A5Icmjxk+jG9nMSnRPlAozJfTCXu/Oy\nfkI4CcClGQv7U1EzfknBVNEKmuPO8XlEkGZ5lW/TzWk8ZtQqJsVuqZlZ5qQE\n/p8FNYEZ5RHPhhKlotA+T6XKh23z9mcN/JNEsSNcx+guERbivtTwnd5JpxQJ\nMNWzUYh+K0W7LgxoAo2jevDd4CZ/0sAAViIl+QrShodseV9agbldGUUoIMzn\nHUuIV6VYi7X82eKQXDInrtPc9IHekbDDFoncLnsGGrEqD/8O/qOHHOPwNwU+\n+cHATAOUTDv/ccWq1QEIALl8SNOQBmCuanAceUTSwCkM1fC3Ddqa8ZmnMMyG\nnDWDI7XkFU31CO10lN20/kAWjdN2073B/NT+17cp8fCqbQ1pAFJHpabdqmbI\ntm3pCC5M6otTN+MhjgFYcBuxo0rq9qtuEzz5j4Ub9MIIJTurUHMEPMI462Dc\ndK/d8BvDkU7q67Lkp65vpe9e/pv0lMMrQjdohnTHNgbZhbI/Z5LU0ApD//Ye\ncSC26BRUMJITiGKb9pKGAi0/ig0jJfzykgEarzOsY/v0W7016AMka+NsHuNc\n7Qfyg2LApF5s9Z6aK+Uy851haUk+p+abjBdcWACziAmimVGjlRYm49ra4UJ7\nc6LSeAElM584e4Z3VnqajAJWbmWt2atgBQmcwEBBsdNAtzIagNydMbBBIELA\n5wGUbMXfQxrMOo/Mdac/5EMTHT0/kVQNIBHtpX5SknWvbc5DjIHoH3+BbL87\neaxhKW94hMubKNKT3dbm4PKHtsMiS3TGkFZ3GjvA0Cy/Kw==\n=b1N8\n-----END PGP MESSAGE-----\n", - "Signature": "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwsBcBAEBCAAGBQJcxtSkAAoJEGfweL9m9sQefz8IAJhpEOvG7d+PVgq3bEL+\nXzY8yTUKB1ZXEMbcR/uHUWSfu0F8zy4CNtSG5HUVjNxx1xzzJVJcxWB/ljO+\n/bJSFOFexTyNh8i/xU+CiBfm5RhAFTYF9xFwfD3LKp5gaalJAhWhArk1/Wuh\nWTDpFpk39uzBRKNwcnSgiJYPxOjAZxj+w/hhHPwmco5cUwMiMR5MNrfzKf+x\nwX3Cfs9fsiiCDzohBzbK0FFsMnJ8aXNVsDBjEA1KrB7sdyaf8FnaM0RsFRDb\njxtIgfcFBbyNJh4414Unt4AYTIrIWhK4OOXI3AfsJy8p6KRBKQUcUkKcDxab\nPTXOPZsZ+UgQ5MevyVnP1zfCwFwEAQEIAAYFAlzG1KQACgkQm1DMMBzxetx/\nPwf9F04uHtix0zDpP1IvG4VYlor4rjYTdfXqxxiFXHO6MZXJoigS1E71E8r4\nsqZ6PoQ5/xCj2A01KRhuF1Bon3mEZEwaIUuBqTV91sLsVWfoxgyPAYpr6gK/\n1W9JhVNNrVRMGox42LQUjyiq0ESrfWmqC8SuMfZMoUoBZycHicA50RbyOUnT\nLO57ArL3JIVmYtyosaXM3idzmNHmaXSkcGt4cvTVysJZQrneaxmikfm5CH2O\n1z2goLBNnzsbRionoV6gCukZOiM/d14yiyeYtsFJ7u/vodkI5y0M0sF8VnN/\njL9keP4ZpHiJ4MBd6tyIH0pLueDRlurFL3fcHsEzD5SrOA==\n=wGCk\n-----END PGP SIGNATURE-----\n" - } -] diff --git a/pkg/pmapi/testdata/keyring_addressKeysWithTokens_JSON b/pkg/pmapi/testdata/keyring_addressKeysWithTokens_JSON deleted file mode 100644 index 97baa34f..00000000 --- a/pkg/pmapi/testdata/keyring_addressKeysWithTokens_JSON +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "ID": "hKRtZeTDhvzfAaycb5BOVx6Y3hc3gs4QvET8H_YZBTwAQBPp3h6FI4nnkJePCYuM9CG0zf7TQzOJeB2rPi0YmQ==", - "Primary": 1, - "Active": 1, - "Flags": 3, - "Version": 3, - "Activation": null, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nxcMGBFzGzj4BCADh9uuhG3mDhlbFMgQpvP6AReyJYa6KSV9a6AJsNI0/DkVz\nFenIodav/r4RBgSoblqxCa/QH6galbX0eB8Vtxx4xTXCcHHZzTM9oAb4s8r6\ndGchNsSkoLspVwmc2fYfDRCTcapmeza7Jym8gD9wFUSWSwpC49jDeT6VIcOd\n+qNXgIJX1V36BOAB53lKenMYnxfzzAyJDeL/sQ/SWE9h/X+wGdkVmk7gk3pa\nQ9URoxAf7MO808hHyQYcjTV4tOHD/9k+AZC6sqmr2KAefYCxCMoCDj1wQp3J\nN6MfK2Zvc1r5KFQM2YzUXrMNrycAjo7gGeZnsvz758Q+8ouSP7iXGRl/ABEB\nAAH+CQMIQt3QQIJZYjRgTVgIQNSEkhMn4GJ5lQQPBXD0/YyK3xP0bm/bsX8S\nz9dAm1nkP0l8Q9z3lR6OcAX2O/KsqJdQGmJEZAaadz2je+EEWo1FDYaL76E6\n6z+AuteP8UtP61jBt3pTfIlhW8o7o2SlM1lvytpvc5FQplZ/iUuwfsIltCiD\n9VDkprNOVrRsjoh8BV4TrrIvU8qga58Aeg7SjOmm+3oMZ7yPTYirr11Tx/m3\nj1uzdEDfiPk4LmvlzSsWwKZuy6fSul+n92+9qN81wmdts/I2ucuKvOINZim4\nlk/p2AOsPjWpAgkefpTVLZnu2IH+VAyaXt1Fl84badXx4N921nPs7ova1Ud5\n2RddBc7b/01DtOyBSWDoNskLGpsc2mqz9kdkwwQKNjChzZc9nmY9M+AIfgT3\n+2DSQIuoJYPX69DKi/bZDwRzoHmiwiHT6Us7qxd6kD1dzCIHTptxwZQp4Tow\nnN6lmtK4S6O2B47+ROn8s0N+EH9GR8F6mvOTayNLH5yicpR3M4Of0ClvFa0G\n+JUKBXIQXUvF03G3nTPU17nLibC81UmbK3zobfbrLfuU2gU+sY+OfE1E/+GO\nSFpZcrkoRqRr39CfLkLk+GjU7RCLNddb5LxgaurVZo5h0Y7Rr8VvOQMWjjl/\nvTAG7gU/HtXi24TijNC0fP6j9w43K4b6t1SZYn5us7RRlFlWGKMSPf5Q9j6/\nryo3xULUQv0lTCQPtfQQ5UWE5ZpQ4Kjt/k5+/YtfuOcMbrDU4qa+H+rrisBu\nko7f4Wn0iYjRwRIuWh1NfUM3rIbNhq7/wonasEFOeFdPwprzMaawx3rL0Pq5\nGs/4LONqG61c9rBekbkGf7Jlkuq/5yo5RBgPnKwvJKsHqf1evD6kHC3aOfeO\n30UoMwe7g763pOXZrsOpZfPzxmraJJSYzS0iYXJvbjIxLTRAc2FkZW1iZS5v\ncmciIDxhcm9uMjEtNEBzYWRlbWJlLm9yZz7CwHUEEAEIAB8FAlzGzj4GCwkH\nCAMCBBUICgIDFgIBAhkBAhsDAh4BAAoJEC3c5LbRFpy6lOgH/RKA4QaTnCi8\nc7HHhDZncqwBSUNhTjCAvoiireX9gGm9JugxaPxHVH4RzznY6R7/Ui8Ak7S8\n+k/xhHbsOGc2exyWwUN1X3WJY3jSX2HNqDU4qw6hSwBGReYbOWJeKGhJWild\nPS4V6u6manGWXxQmW1XET3B0P72VJVSX1kUbslPBAhKbW3JCnDmEdV/sU6Ds\nXdh543Yph1OpO2Fq7b/+YYUDAzmHf/+k4ijVcvqjrjUCJJwv+2J9woi4ToW6\n3BQVG5gpAYzCfgoJjlaigInhoFrBjP25Oe6/ssDTssGJrHXhtyc8e+b7nm19\nSHOpWGcUn2F1+tU+E4O8SLCLGJxefJvHwwYEXMbOPgEIAKEkpHRBhTWOIeYB\ngPXh/Ng77x3Bs6EKwTQM/BePYC2uS+15+nIpiYHhb/sQ9aEbQgqmyfbkbfIf\n9Qahx4N9RFyqWcmSjfk0Bmo9xOziRQm1tfYkbUwkeI6NIr2ENUWVf7tt+UKz\n5dFmvSKsyrdPEtt+Ken17JoihhJ/9saLMkLn5Y3HrSGVXIniX1cQuarXGX9S\nyt7jIPeGZW2suuxlnlB6Sa/rCkaqR3C3a8knxiH8CDwAm+E8a1d/UbQ0np0k\nqTVVrc2fmxPMgZxGWrwIsO9D1Fs7dgw5rac7ijHvPXeWrzbMd1+rX5mmLF4+\nH2PDOUKN0Uu8WdVilCIEOMoVfY0AEQEAAf4JAwiPkDWd2zmFhmBFMZgWR+X2\nhS4q8bIohwFb8MDzrHAvtI8LOaxC32j3/tUwJKDwLNDeiiOGYfHUOmqgzVRi\nMBdkoVNT3OyFLbw6k/70spe5OcecZsM+OAQybX77Kv3H/VW/40TtUh52Zvvk\nqCoqtG86C4R5kma0zM+yvNyprkCIRwYuJ+OkdmMvem3fRqU49GNzJChBDAmZ\nfGc7W7r2JFIo1k88bh+kaGlDBA9p8GnA7KNXAlBdq6owJNJA0j2z6Rmngpip\nFuUEjdW8Kcs2ben4BZOWGgYmOmD1CAHRYm/xp+Us1kJDTTFxW7P7BNp1kVOP\n9s9AcV/t37bCHLVB2IjAb/tkOxyAexMTS/lFJrpJ89MpYiUt70uB+SDwRls9\noud9CQYS+6OdddgFUjtnOkpcR1Y24v1eF13JwUXmnPggvt6Do3gS6lGdq0Nt\nqNDg8+yPyMA8365Q8IbJDuzm3vfc560Szc/Kx4+1zr57Uaw/qYqhZHSkjRtZ\nfTP0v0ZNwxyXhGF3J31neJ5KgIO+zpSWSP8RwtUVBb8Tsyn8e/DSe/8fAs3V\ntmyxSj9mPQLxW8JCQMpocExEJV7PnxjhB0d1TlYu/wRUl4sxpS4pYa/TDhb9\n37oUYtG28TufT7AZE+XcU8Y1Xl7DVi5jgadUAI5rY71G31JeBuIcGRlVLBnT\nSHnu1v2iz1xqZKQFynH6DUrySn8nP8NM9TaVEnSBlVCjHcyvrs8crx358YFV\n4TG44XQ3n5GLjPetKGD/ccMYOUZm2jkLNzY9l4YqKJq7xsm2c5VhwUEB51Pi\n8Ey+x7hO3EjWUgJrtI/3/hPoKlltPUvnhJjNrR8ivocvd2v+2U0BHQOwTUt3\njSDssFDI9hfOMPp4yy/GHM+p3USC+3TMS8HjFbm6b2bIWzqEv83AaauZqiE1\nfJfCwF8EGAEIAAkFAlzGzj4CGwwACgkQLdzkttEWnLqI8Af/Uimov/qMfFdi\nSp1qYTzR9V84ZkfxBAcr9nEyNGtMlvCwrs3EtZPBe7ou57Qt9aI8UjJfsiov\naHUC1Gt8266T1+GGj6RCPSm6Sp7cZxGtURZDWAPb5u02+VtrqrrXQgkCxQ8/\nzWfg4vLyl052x+3F9SGy7SvH0qi8bcHzVjcwK9VyoSdBq/vhkPHQ33wLQ9ND\nuQCM8fxW+VOqMDlUCHfdcaYRYU7GEm9C37ZijpOLZRuAm6ojjCGtrOrGQ+y0\ni3q7Zn1yDradMlMGG9GjDlqOCmYhZbuT4uS578GzPg0L8zk/1rFOF+YY6Mfq\nV0GB3IX8qBHjAfPmqN9JPxBIn3/6ag==\n=U1yD\n-----END PGP PRIVATE KEY BLOCK-----\n", - "Token": "-----BEGIN PGP MESSAGE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwcBMAxW/uQhm1PKxAQf8D4BAoXqnYgoTr7jEiabP9NkeuD6Z88fZDgUzXte5\n6aYMmCtPr0Vef1eDl2x0S0q/YJgR+A5Icmjxk+jG9nMSnRPlAozJfTCXu/Oy\nfkI4CcClGQv7U1EzfknBVNEKmuPO8XlEkGZ5lW/TzWk8ZtQqJsVuqZlZ5qQE\n/p8FNYEZ5RHPhhKlotA+T6XKh23z9mcN/JNEsSNcx+guERbivtTwnd5JpxQJ\nMNWzUYh+K0W7LgxoAo2jevDd4CZ/0sAAViIl+QrShodseV9agbldGUUoIMzn\nHUuIV6VYi7X82eKQXDInrtPc9IHekbDDFoncLnsGGrEqD/8O/qOHHOPwNwU+\n+cHATAOUTDv/ccWq1QEIALl8SNOQBmCuanAceUTSwCkM1fC3Ddqa8ZmnMMyG\nnDWDI7XkFU31CO10lN20/kAWjdN2073B/NT+17cp8fCqbQ1pAFJHpabdqmbI\ntm3pCC5M6otTN+MhjgFYcBuxo0rq9qtuEzz5j4Ub9MIIJTurUHMEPMI462Dc\ndK/d8BvDkU7q67Lkp65vpe9e/pv0lMMrQjdohnTHNgbZhbI/Z5LU0ApD//Ye\ncSC26BRUMJITiGKb9pKGAi0/ig0jJfzykgEarzOsY/v0W7016AMka+NsHuNc\n7Qfyg2LApF5s9Z6aK+Uy851haUk+p+abjBdcWACziAmimVGjlRYm49ra4UJ7\nc6LSeAElM584e4Z3VnqajAJWbmWt2atgBQmcwEBBsdNAtzIagNydMbBBIELA\n5wGUbMXfQxrMOo/Mdac/5EMTHT0/kVQNIBHtpX5SknWvbc5DjIHoH3+BbL87\neaxhKW94hMubKNKT3dbm4PKHtsMiS3TGkFZ3GjvA0Cy/Kw==\n=b1N8\n-----END PGP MESSAGE-----\n", - "Signature": "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwsBcBAEBCAAGBQJcxtSkAAoJEGfweL9m9sQefz8IAJhpEOvG7d+PVgq3bEL+\nXzY8yTUKB1ZXEMbcR/uHUWSfu0F8zy4CNtSG5HUVjNxx1xzzJVJcxWB/ljO+\n/bJSFOFexTyNh8i/xU+CiBfm5RhAFTYF9xFwfD3LKp5gaalJAhWhArk1/Wuh\nWTDpFpk39uzBRKNwcnSgiJYPxOjAZxj+w/hhHPwmco5cUwMiMR5MNrfzKf+x\nwX3Cfs9fsiiCDzohBzbK0FFsMnJ8aXNVsDBjEA1KrB7sdyaf8FnaM0RsFRDb\njxtIgfcFBbyNJh4414Unt4AYTIrIWhK4OOXI3AfsJy8p6KRBKQUcUkKcDxab\nPTXOPZsZ+UgQ5MevyVnP1zfCwFwEAQEIAAYFAlzG1KQACgkQm1DMMBzxetx/\nPwf9F04uHtix0zDpP1IvG4VYlor4rjYTdfXqxxiFXHO6MZXJoigS1E71E8r4\nsqZ6PoQ5/xCj2A01KRhuF1Bon3mEZEwaIUuBqTV91sLsVWfoxgyPAYpr6gK/\n1W9JhVNNrVRMGox42LQUjyiq0ESrfWmqC8SuMfZMoUoBZycHicA50RbyOUnT\nLO57ArL3JIVmYtyosaXM3idzmNHmaXSkcGt4cvTVysJZQrneaxmikfm5CH2O\n1z2goLBNnzsbRionoV6gCukZOiM/d14yiyeYtsFJ7u/vodkI5y0M0sF8VnN/\njL9keP4ZpHiJ4MBd6tyIH0pLueDRlurFL3fcHsEzD5SrOA==\n=wGCk\n-----END PGP SIGNATURE-----\n" - } -] diff --git a/pkg/pmapi/testdata/keyring_addressKeysWithoutTokens_JSON b/pkg/pmapi/testdata/keyring_addressKeysWithoutTokens_JSON deleted file mode 100644 index adce766c..00000000 --- a/pkg/pmapi/testdata/keyring_addressKeysWithoutTokens_JSON +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "ID": "MhF8dxtN5Lz_GruskN3L9kTKWusZHBdhWaxc7w1tgze2qB6uM9AyC6mWRQg1B7WGZ_r-9gn-XC95-IkpNJr0jg==", - "Primary": 1, - "Active": 1, - "Flags": 3, - "Version": 3, - "Activation": null, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlQPGBF2IcLUBCACl7vans5JKYAEuOZfKNDLZKmLO675DhLjbno8Pw4Q8O6xQETUZ\nTFsIB6eTigCqaZasCeHIx4r7CI4TY2uZvV3+JNaAHN0omHh3iwVXQiE3FU5Zc6TO\niVswOoWPbVYFTuItP3FQHuossMIralIzG0/AyDx0NJawwpSxxz9+C0UoYeZD1A95\nfKveRVhDW0MgO1peDHrpr2HiWv06KkIop39CD/MBnBtDpsarGd16wvX+r8F3MrNE\nD1/e6szzi7oR5H0hPs+HSWZexXhiGSWoEyMiklX/sG/9+LxnHqPhCj7lrGfzmL7j\nCFuHZIqpIO8wVggOQYx1mtmMu7T9PZm66WMbABEBAAH+BwMCYpZ0ni+oe4DxIByk\nseTvdCIjQ3ieiTVvg9FsIVrupmIxDK9EVsGw1/sFjBtc5OOP8N9Vd8IyPh6WjBMu\n5pI5dsYKCLy6LMDmYTYQzXgWlpq2cpH+yZX21fZ+0ijEu+TTSJss3emCULrtCS5M\nUTzt813cIPJu8bYPdQ7taVRqK0Bo+S8YTj2a+lxHQRkbRclueuTm7l5ypU/9X4Xf\n4zwUKnq0OYu3U8IkDEOA+4Zv4SZssFrPr8plUzMHUahVV7NZCXOG11ju1CC9CkF2\n3lpkCZw96TwCGQnT4iTPix1NEJZo6ihRbMlnLo/neS3TWgdCpK5Qr8x4B82ykX6J\ngUeNozW/YFOwFwah22TeSN2dbaGbzJOSqMXHVzK9mz/oTajY/51pS/ZxGoxgy7F2\nD8E/m0I7dS3fw/A/6ZtQFa1bW8L/dBnN71gfr6PSVxn3oczPCXtzNeqHPaDIDGV1\nhgGbGFgmjJy9mnPuxQcScIJloLOVerAaqMcgFuo2MhGKLzQ7IGiSzinaJZ7k602f\nJPfpqBg2aMO9xxNC0S6p6KzMRZCjM1cOJYF2jMo2UWqM0XA1+aEqPksBTmyKSd9+\n1sc8coVl3iesji6GZoT5s0+sNEiQZuNOFUzmGKdgqZhDVfPTLmfV1Uv/tlZCGKmC\ntC51VGjqf8RE3ycdn0Hby8Q2DLsaTnC2vMUde2iJwGQU41TTlt4TKSp6YtqSxbp+\nzUpF4QuS2zVARtx3Z87vw6rHmrNCXHzxl9zWjF3M42FhEESrUL0dciM3c1pB5twd\nIx1TSI0+96zneGS4AkN5Tqua+RdvWXX8ljRgLFa8DqraumQyOjyMXRlJoiey1dt3\n3sRfeF8eI2QF9DooI+4Za/C/1VaEURSQ4p04Ng/xJJ9mwpxjKvxmujC9n2yZ1u4o\nvQh9cKQ9nQAItDdqYW1lcy10ZXN0QHByb3Rvbm1haWwuYmx1ZSA8amFtZXMtdGVz\ndEBwcm90b25tYWlsLmJsdWU+iQE1BBABCAAfBQJdiHC1BgsJBwgDAgQVCAoCAxYC\nAQIZAQIbAwIeAQAKCRC/qkLbur13GVuWB/410LkZxZltVA1odI+yL3utIhoNWj8R\n3AhlVPLfAymkJOFv2KMZ2pRkS8PTmq4sYjAlti60cpWwrNdU4eSfNxoWDEg2lILW\nFULm3TwygDFogSsqyKlGXGr0MqnegImDeQg2kMLPvudTNvEbnRDpaCxWUHenEH5+\nTb8+UyG1e3V2su5J6p+Ghh88Y+zJyU4DWta3wAymLPW+gUEFXDAw5KdSg/8iD5kH\n6aMbd9l1Wp5WiErMsJOwoxhFVhwUK7MCXAXxHMWTuyu78RYYNhafPpd+Rq6dqy1d\nm5M7BlDU4Y2Y921xOSHGbNXbWbh5WHBT0QkV7IJunwiPYc5apJbee69DnQPGBF2I\ncLUBCAC9RsknEX9jPP4AB2/DxWdqTKb1/AmDpzNstEUtSy+zUKmErSNQeNUuso+n\nH2lFtb/A+s3nC2UPqbgz/SWN37Z9ip6hvsQX0etFvzYiXFfVDbknqmpi8AGt1Bkr\nFP9jGvF7gIT436icfjd8vQHHMIdEbEF+S46LfW8xibZEYUHLr5m8VO7dqqsIIwvr\nTJWEGjtRfaacqV3aWDzHbXodmYC3kQH9UjpdXE/9/5VxWtxDAFwsCaXiE9+EL69Z\ncJDdPfM0iVL3XgP22EFwC8C+Xha1PiSNOGFTIbv4aT5yLgPb7wwvgaw3semLF1vM\nzni3z+FNPREt+9itIhGPpxmIyAQ/ABEBAAH+BwMCl4RZZJs4k1bxqOmzniBxB2y8\nX4sDxyy60IOO3Ip2cOeyJ/qwEBdXxL2EjS6y5Vt3GTnQDz1IJqYiz9eRbZQnXUh5\nSk9ND9+mgxKYHDV52flh2FBTaOgqsKLnwLRUE12D1BQ9Ou5cYcgOIbCncftQTnWV\nk6hg9PDFZD/LD3IuKS9/DVYxKkYIJZ1KH446bQTYto+fnU0ajkLfED7wN4JquHq4\nYS+L7h8GetOCyRsh8zDIOPF5jZYCSzDe8Q03ktozOE2+YV/A3Z/0Uub8/Qecl5u3\n1qURXeIC6oGRDVXegZu3491IG+UTzjaBDPu+Xk4zad7mGjBQ9vNuKZfNNg/nHOWL\nNHRCXbkQVpXCA+v+dNYov+bqKe1QpF/2jvU0Bi32HLG8gfmRwD0WaAnbvOLTYXPl\ndRNDn7ixfOC0uHT6epPlZxsRja5TXlFl5qSuAX2zQfFdb+pRPztNKer6iMNz3itz\nh2B/gcTwqXloy4Zq9wqxcXLV1qmoMJ5YzRcd4otRM7FhZD0LxBAOANdmPSmRHbf6\nSXyXgvK4KDwRB52uIYJzWyIi7LiU3qTbV0GgptgHeVLwgbA9ioPwIu85+XQEh/eE\nEjNhz0CdtCqmYY3+SjNOZ8K59LgWCuv/YkRkmwDimEz5gUlpayoOYib4J4wIx6OY\n+fmI+gByS+UN3v+ml7l909c2W8w0dwR8+7P1XGBkaiVROTM2hdnHMflWm2548WBx\na6tbdCQPUDra/5O/pCdefc44hwD08liutq9kF29sMwONZy0P0dlyFZq4LTLmQqs0\nq93GZRjoI+uit9oTLGCtJ99aiSIx97KfkKx5wLnE4DruQlqPWnxV5Yfn9RZ6j8V2\nRQjL8f5RbiBtkkCl3r+nEdQIeMRy+8xxO9dTYTlaCNgfd92LdQrwzKe+GneUgKYg\nWo+AiQEfBBgBCAAJBQJdiHC1AhsMAAoJEL+qQtu6vXcZskIH+we2sHxPnWOfkXTu\npmZBU1ba8JLcrx+CMwTPkt7GMYpDplVOybn+AjMxB9JxST71mBw6rbwd2XLM1AmJ\nbvXVfmCtmC2TmrORqcoxEql+9tdgnSTfZfrltnYeVuEAvAxrMmD4S2Mb5zVWFl+q\ni69A0rdD8a36nOlJnZxfs57W1zfvl/rh+/RdybPwx9y0hSnRPSypis2dcwSyBD9+\nnlbn3QoybaUJxWvL+9MphCXZ4CuuhG0VPcmdH+LzOytDTtJnDNm+Ru4sokroDJXe\n5XlrK3+wtSNL4rfa/MQYyK8XsWRVR/a0BPeqZA4cPlHqCOSfUfaFbORVbPaQdq2a\nYYsMDlA=\n=ZwJf\n-----END PGP PRIVATE KEY BLOCK-----", - "Token": null, - "Signature": null - } -] diff --git a/pkg/pmapi/testdata/keyring_userKey b/pkg/pmapi/testdata/keyring_userKey deleted file mode 100644 index 976d2be2..00000000 --- a/pkg/pmapi/testdata/keyring_userKey +++ /dev/null @@ -1,62 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: OpenPGP.js v4.4.5 -Comment: testpassphrase - -xcLYBFzGzhEBCADBxfqTFMqfQzT77A5tuuhPFwPq8dfC2evs8u1OvTqFbztY -5FOuSxzduyeDqQ1Fx6dKEOKgcYE8t1Uh4VSS7z6bTdY8j9yrL81kCVB46sE1 -OzStzyx/5l7OdH/pM4F+aKslnLvqlw0UeJr+UNizVtOCEUaNfVjPK3cc1ocx -v+36K4RnnyfEtjUW9gDZbhgaF02G5ILHmWmbgM7I+77gCd2wI0EdY9s/JZQ+ -VmkMFqoMdY9PyBchoOIPUkkGQi1SaF4IEzMaAUSbnCYkHHY/SbfDTcR46VGq -cXlkB1rq5xskaUQ9r+giCC/K4pc7bBkI1lQ7ADVuWvdrWnWapK0FO6CfABEB -AAEAB/0YPhPJ0phA/EWviN+16bmGVOZNaVapjt2zMMybWmrtEQv3OeWgO3nP -4cohRi/zaCBCphcm+dxbLhftW7AFi/9PVcR09436MB+oTCQFugpUWw+4TmA5 -BidxTpDxf4X2vH3rquQLBufWL6U7JlPeKAGL1xZ2aCq0DIeOk5D+xTjZizV2 -GIyQRVCLWb+LfDmvvcp3Y94X60KXdBAMuS1ZMKcY3Sl8VAXNB4KQsC/kByzf -6FCB097XZRYV7lvJJQ7+6Wisb3yVi8sEQx2sFm5fAp+0qi3a6zRTEp49r6Hr -gyWViH5zOOpA7DcNwx1Bwhi7GG0tak6EUnnKUNLfOupglcphBADmpXCgT4nc -uSBYTiZSVcB/ICCkTxVsHL1WcXtPK2Ikzussx2n9kb0rapvuC0YLipX9lUkQ -fyeC3jQJeCyN79AkDGkOfWaESueT2hM0Po+RwDgMibKn6yJ1zebz4Lc2J3C9 -oVFcAnql+9KyGsAPn03fyQzDnvhNnJvHJi4Hx8AWoQQA1xLoXeVBjRi0IjjU -E6Mqaq5RLEog4kXRp86VSSEGHBwyIYnDiM//gjseo/CXuVyHwL7UXitp8s1B -D1uE3APrhqUS66fD5pkF+z+RcSqiIv7I76NJ24Cdg38L6seGSjOHrq7/dEeG -K6WqfQUCEjta3yNSg7pXb2wn2WZqKIK+rz8EALZRuMXeql/FtO3Cjb0sv7oT -9dLP4cn1bskGRJ+Vok9lfCERbfXGccoAk3V+qSfpHgKxsebkRbUhf+trOGnw -tW+kBWo/5hYGQuN+A9JogSJViT+nuZyE+x1/rKswDFmlMSdf2GIDARWIV0gc -b1yOEwUmNBSthPcnFXvBr4BG3XTtNPTNLSJhcm9uMjEtM0BzYWRlbWJlLm9y -ZyIgPGFyb24yMS0zQHNhZGVtYmUub3JnPsLAdQQQAQgAHwUCXMbOEQYLCQcI -AwIEFQgKAgMWAgECGQECGwMCHgEACgkQZ/B4v2b2xB6XUgf/dHGRHimyMR78 -QYbEm2cuaEvOtq4a+J6Zv3P4VOWAbvkGWS9LDKSvVi60vq4oYOmF54HgPzur -nA4OtZDf0HKwQK45VZ7CYD693o70jkKPrAAJG3yTsbesfiS7RbFyGKzKJ7EL -nsUIJkfgm/SlKmXU/u8MOBO5Wg7/TcsS33sRWHl90j+9jbhqdl92R+vY/CwC -ieFkQA7/TDv1u+NAalH+Lpkd8AIuEcki+TAogZ7oi/SnofwnoB7BxRm+mIkp -ZZhIDSCaPOzLG8CSZ81d3HVHhqbf8dh0DFKFoUYyKdbOqIkNWWASf+c/ZEme -IWcekY8hqwf/raZ56tGM/bRwYPcotMfC1wRcxs4RAQgAsMb5/ELWmrfPy3ba -5qif+RXhGSbjitATNgHpoPUHrfTC7cn4JWHqehoXLAQpFAoKd+O/ZNpZozK9 -ilpqGUx05yMw06jNQEhYIbgIF4wzPpz02Lp6YeMwdF5LF+Rw83PHdHrA/wRV -/QjL04+kZnN+G5HmzMlhFY+oZSpL+Gp1bTXgtAVDkhCnMB5tP2VwULMGyJ+X -vRYxwTK2CrLjIVZv5n1VYY+caCowU6j/XFqvlCJj+G5oV+UhFOWffaMRXhOh -a64RrhqT1Np7wCLvLMP2wpys9xlMcLQJLqDNxqOTp504V7dm67ncC0fKUsT4 -m4oTktnxKPd6MU+4VYveaLCquwARAQABAAf4u9s7gpGErs1USxmDO9TlyGZK -aBlri8nMf3s+hOJCOo3cRaRHJBfdY6pu/baG6H6JTsWzeY4MHwr6N+dhVIEh -FPMa9EZAjagyc4GugxWGiMVTfU+2AEfdrdynhQKMgXSctnnNCdkRuX0nwqb3 -nlupm1hsz2ze4+Wg0BKSLS0FQdoUbITdJUR69OHr4dNJVHWYI0JSBx4SdhV3 -y9163dDvmc+lW9AEaD53vyZWfzCHZxsR/gI32VmT0z5gn1t8w9AOdXo2lA1H -bf7wh4/qCyujGu64ToZtiEny/GCyM6PofLtiZuJNLw3s/y+B2tKv22aTJ760 -+Gib1xB9WcWjKyrxBADoeCyq+nHGrl0CwOkmjanlFymgo7mnBOXuiFOvGrKk -M1meMU1TI4TEBWkVnDVMcSejgjAf/bX1dtouba1tMAMu7DlaV/0EwbSADRel -RSqEbIzIOys+y9TY/BMI/uCKNyEKHvu1KUXADb+CBpdBpCfMBWDANFlo9xLz -Ajcmu2dyawQAwquwC0VXQcvzfs+Hd5au5XvHdm1KidOiAdu6PH1SrOgenIN4 -lkEjHrJD9jmloO2/GVcxDBB2pmf0B4HEg7DuY9LXBrksP5eSbbRc5+UH1HUv -u82AqQnfNKTd/jae+lLwaOS++ohtwMkkD6W0LdWnHPjyyXg4Oi9zPID3asRu -3PED/3CYyjl6S8GTMY4FNH7Yxu9+NV2xpKE92Hf8K/hnYlmSSVKDCEeOJtLt -BkkcSqY6liCNSMmJdVyAF2GrR+zmDac7UQRssf57oOWtSsGozt0aqJXuspMT -6aB+P1UhZ8Ly9rWZNiJ0jwyfnQNOLCYDaqjFmiSpqrNnJ2Q1Xge3+k80P9DC -wF8EGAEIAAkFAlzGzhECGwwACgkQZ/B4v2b2xB5wlwgAjZA1zdv5irFjyWVo -4/itONtyO1NbdpyYpcct7vD0oV+a4wahQP0J3Kk1GhZ5tvAoZF/jakQQOM5o -GjUYpXAGnr09Mv9EiQ2pDwXc2yq0WfXnGxNrpzOqdtV+IqY9NYkl55Tme7x+ -WRvrkPSUeUsyEGvxwR1stdv8eg9jUmxdl8Io3PYoFJJlrM/6aXeC1r3KOj7q -XAnR0XHJ+QBSNKCWLlQv5hui9BKfcLiVKFK/dNhs82nRyhPr4sWFw6MTqdAK -4zkn7l0jmy6Evi1AiiGPiHPnxeNErnofOIEh4REQj00deZADHrixTLtx2FuR -uaSC3IcBmBsj1fNb4eYXElILjQ== -=fMOl ------END PGP PRIVATE KEY BLOCK----- \ No newline at end of file diff --git a/pkg/pmapi/testdata/keyring_userKey_JSON b/pkg/pmapi/testdata/keyring_userKey_JSON deleted file mode 100644 index 01c912dd..00000000 --- a/pkg/pmapi/testdata/keyring_userKey_JSON +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==", - "Version": 3, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: OpenPGP.js v0.7.1\r\nComment: http://openpgpjs.org\r\n\r\nxcMGBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE\nWSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39\nvPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi\nMeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5\nc8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb\nDEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB\nAAH+CQMIvzcDReuJkc9gnxAkfgmnkBFwRQrqT/4UAPOF8WGVo0uNvDo7Snlk\nqWsJS+54+/Xx6Jur/PdBWeEu+6+6GnppYuvsaT0D0nFdFhF6pjng+02IOxfG\nqlYXYcW4hRru3BfvJlSvU2LL/Z/ooBnw3T5vqd0eFHKrvabUuwf0x3+K/sru\nFp24rl2PU+bzQlUgKpWzKDmO+0RdKQ6KVCyCDMIXaAkALwNffAvYxI0wnb2y\nWAV/bGn1ODnszOYPk3pEMR6kKSxLLaO69kYx4eTERFyJ+1puAxEPCk3Cfeif\nyDWi4rU03YB16XH7hQLSFl61SKeIYlkKmkO5Hk1ybi/BhvOGBPVeGGbxWnwI\n46G8DfBHW0+uvD5cAQtk2d/q3Ge1I+DIyvuRCcSu0XSBNv/Bkpp4IbAUPBaW\nTIvf5p9oxw+AjrMtTtcdSiee1S6CvMMaHhVD7SI6qGA8GqwaXueeLuEXa0Ok\nBWlehx8wibMi4a9fLcQZtzJkmGhR1WzXcJfiEg32srILwIzPQYxuFdZZ2elb\ngYp/bMEIp4LKhi43IyM6peCDHDzEba8NuOSd0heEqFIm0vlXujMhkyMUvDBv\nH0V5On4aMuw/aSEKcAdbazppOru/W1ndyFa5ZHQIC19g72ZaDVyYjPyvNgOV\nAFqO4o3IbC5z31zMlTtMbAq2RG9svwUVejn0tmF6UPluTe0U1NuXFpLK6TCH\nwqocLz4ecptfJQulpYjClVLgzaYGDuKwQpIwPWg5G/DtKSCGNtEkfqB3aemH\nV5xmoYm1v5CQZAEvvsrLA6jxCk9lzqYV8QMivWNXUG+mneIEM35G0HOPzXca\nLLyB+N8Zxioc9DPGfdbcxXuVgOKRepbkq4xv1pUpMQ4BUmlkejDRSP+5SIR3\niEthg+FU6GRSQbORE6nhrKjGBk8fpNpozQZVc2VySUTCwHIEEAEIACYFAlRJ\nbc8GCwkIBwMCCRA+tiWe3yHfJAQVCAIKAxYCAQIbAwIeAQAA9J0H/RLR/Uwt\nCakrPKtfeGaNuOI45SRTNxM8TklC6tM28sJSzkX8qKPzvI1PxyLhs/i0/fCQ\n7Z5bU6n41oLuqUt2S9vy+ABlChKAeziOqCHUcMzHOtbKiPkKW88aO687nx+A\nol2XOnMTkVIC+edMUgnKp6tKtZnbO4ea6Cg88TFuli4hLHNXTfCECswuxHOc\nAO1OKDRrCd08iPI5CLNCIV60QnduitE1vF6ehgrH25Vl6LEdd8vPVlTYAvsa\n6ySk2RIrHNLUZZ3iII3MBFL8HyINp/XA1BQP+QbH801uSLq8agxM4iFT9C+O\nD147SawUGhjD5RG7T+YtqItzgA1V9l277EXHwwYEVEltzwEIAJD57uX6bOc4\nTgf3utfL/4hdyoqIMVHkYQOvE27wPsZxX08QsdlaNeGji9Ap2ifIDuckUqn6\nJi9jtZDKtOzdTBm6rnG5nPmkn6BJXPhnecQRP8N0XBISnAGmE4t+bxtts5Wb\nqeMdxJYqMiGqzrLBRJEIDTcg3+QF2Y3RywOqlcXqgG/xX++PsvR1Jiz0rEVP\nTcBc7ytyb/Av7mx1S802HRYGJHOFtVLoPTrtPCvv+DRDK8JzxQW2XSQLlI0M\n9s1tmYhCogYIIqKx9qOTd5mFJ1hJlL6i9xDkvE21qPFASFtww5tiYmUfFaxI\nLwbXPZlQ1I/8fuaUdOxctQ+g40ZgHPcAEQEAAf4JAwgdUg8ubE2BT2DITBD+\nXFgjrnUlQBilbN8/do/36KHuImSPO/GGLzKh4+oXxrvLc5fQLjeO+bzeen4u\nCOCBRO0hG7KpJPhQ6+T02uEF6LegE1sEz5hp6BpKUdPZ1+8799Rylb5kubC5\nIKnLqqpGDbH3hIsmSV3CG/ESkaGMLc/K0ZPt1JRWtUQ9GesXT0v6fdM5GB/L\ncZWFdDoYgZAw5BtymE44knIodfDAYJ4DHnPCh/oilWe1qVTQcNMdtkpBgkuo\nTHecqEmiODQz5EX8pVmS596XsnPO299Lo3TbaHUQo7EC6Au1Au9+b5hC1pDa\nFVCLcproi/Cgch0B/NOCFkVLYmp6BEljRj2dSZRWbO0vgl9kFmJEeiiH41+k\nEAI6PASSKZs3BYLFc2I8mBkcvt90kg4MTBjreuk0uWf1hdH2Rv8zprH4h5Uh\ngjx5nUDX8WXyeLxTU5EBKry+A2DIe0Gm0/waxp6lBlUl+7ra28KYEoHm8Nq/\nN9FCuEhFkFgw6EwUp7jsrFcqBKvmni6jyplm+mJXi3CK+IiNcqub4XPnBI97\nlR19fupB/Y6M7yEaxIM8fTQXmP+x/fe8zRphdo+7o+pJQ3hk5LrrNPK8GEZ6\nDLDOHjZzROhOgBvWtbxRktHk+f5YpuQL+xWd33IV1xYSSHuoAm0Zwt0QJxBs\noFBwJEq1NWM4FxXJBogvzV7KFhl/hXgtvx+GaMv3y8gucj+gE89xVv0XBXjl\n5dy5/PgCI0Id+KAFHyKpJA0N0h8O4xdJoNyIBAwDZ8LHt0vlnLGwcJFR9X7/\nPfWe0PFtC3d7cYY3RopDhnRP7MZs1Wo9nZ4IvlXoEsE2nPkWcns+Wv5Yaewr\ns2ra9ZIK7IIJhqKKgmQtCeiXyFwTq+kfunDnxeCavuWL3HuLKIOZf7P9vXXt\nXgEir9rCwF8EGAEIABMFAlRJbdIJED62JZ7fId8kAhsMAAD+LAf+KT1EpkwH\n0ivTHmYako+6qG6DCtzd3TibWw51cmbY20Ph13NIS/MfBo828S9SXm/sVUzN\n/r7qZgZYfI0/j57tG3BguVGm53qya4bINKyi1RjK6aKo/rrzRkh5ZVD5rVNO\nE2zzvyYAnLUWG9AV1OYDxcgLrXqEMWlqZAo+Wmg7VrTBmdCGs/BPvscNgQRr\n6Gpjgmv9ru6LjRL7vFhEcov/tkBLj+CtaWWFTd1s2vBLOs4rCsD9TT/23vfw\nCnokvvVjKYN5oviy61yhpqF1rWlOsxZ4+2sKW3Pq7JLBtmzsZegTONfcQAf7\nqqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA==\r\n=2wIY\r\n-----END PGP PRIVATE KEY BLOCK-----\r\n", - "Fingerprint": "c93f767df53b0ca8395cfde90483475164ec6353", - "Activation": null, - "Primary": 1, - "Active": 1 - } -] diff --git a/pkg/pmapi/testdata/routes/HTTP_200.json b/pkg/pmapi/testdata/routes/HTTP_200.json deleted file mode 100644 index ebc8f3a6..00000000 --- a/pkg/pmapi/testdata/routes/HTTP_200.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Code": 1000 -} diff --git a/pkg/pmapi/testdata/routes/HTTP_401.json b/pkg/pmapi/testdata/routes/HTTP_401.json deleted file mode 100644 index 6fbbf86f..00000000 --- a/pkg/pmapi/testdata/routes/HTTP_401.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Code": 5000, - "Error": "Status unauthorized" -} diff --git a/pkg/pmapi/testdata/routes/HTTP_402.json b/pkg/pmapi/testdata/routes/HTTP_402.json deleted file mode 100644 index 2b608112..00000000 --- a/pkg/pmapi/testdata/routes/HTTP_402.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Code": 5000, - "Error": "Status payment required" -} diff --git a/pkg/pmapi/testdata/routes/addresses/get_response.json b/pkg/pmapi/testdata/routes/addresses/get_response.json deleted file mode 100644 index 0ebaa6d0..00000000 --- a/pkg/pmapi/testdata/routes/addresses/get_response.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "Code": 1000, - "Addresses": [ - { - "ID": "qmhrlFY24BhSHiFplF0B7G_cMVLi1sokaWIhfNaee6dRtdIZPYnqgI4-MpAb8h3JhOOykKv8ZsuTH8X_SrUZSg==", - "DomainID": "l8vWAXHBQmv0u7OVtPbcqMa4iwQaBqowINSQjPrxAr-Da8fVPKUkUcqAq30_BCxj1X0nW70HQRmAa-rIvzmKUA==", - "Email": "jason@protonmail.com", - "Send": 1, - "Receive": 1, - "Status": 1, - "Type": 1, - "Order": 1, - "DisplayName": "D L'u, P.D. \u5b9a\u8d85", - "Signature": "hi there", - "HasKeys": 1, - "Keys": [ - { - "ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==", - "Version": 3, - "Flags": 3, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: OpenPGP.js v0.7.1\r\nComment: http://openpgpjs.org\r\n\r\nxcMGBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE\nWSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39\nvPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi\nMeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5\nc8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb\nDEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB\nAAH+CQMIvzcDReuJkc9gnxAkfgmnkBFwRQrqT/4UAPOF8WGVo0uNvDo7Snlk\nqWsJS+54+/Xx6Jur/PdBWeEu+6+6GnppYuvsaT0D0nFdFhF6pjng+02IOxfG\nqlYXYcW4hRru3BfvJlSvU2LL/Z/ooBnw3T5vqd0eFHKrvabUuwf0x3+K/sru\nFp24rl2PU+bzQlUgKpWzKDmO+0RdKQ6KVCyCDMIXaAkALwNffAvYxI0wnb2y\nWAV/bGn1ODnszOYPk3pEMR6kKSxLLaO69kYx4eTERFyJ+1puAxEPCk3Cfeif\nyDWi4rU03YB16XH7hQLSFl61SKeIYlkKmkO5Hk1ybi/BhvOGBPVeGGbxWnwI\n46G8DfBHW0+uvD5cAQtk2d/q3Ge1I+DIyvuRCcSu0XSBNv/Bkpp4IbAUPBaW\nTIvf5p9oxw+AjrMtTtcdSiee1S6CvMMaHhVD7SI6qGA8GqwaXueeLuEXa0Ok\nBWlehx8wibMi4a9fLcQZtzJkmGhR1WzXcJfiEg32srILwIzPQYxuFdZZ2elb\ngYp/bMEIp4LKhi43IyM6peCDHDzEba8NuOSd0heEqFIm0vlXujMhkyMUvDBv\nH0V5On4aMuw/aSEKcAdbazppOru/W1ndyFa5ZHQIC19g72ZaDVyYjPyvNgOV\nAFqO4o3IbC5z31zMlTtMbAq2RG9svwUVejn0tmF6UPluTe0U1NuXFpLK6TCH\nwqocLz4ecptfJQulpYjClVLgzaYGDuKwQpIwPWg5G/DtKSCGNtEkfqB3aemH\nV5xmoYm1v5CQZAEvvsrLA6jxCk9lzqYV8QMivWNXUG+mneIEM35G0HOPzXca\nLLyB+N8Zxioc9DPGfdbcxXuVgOKRepbkq4xv1pUpMQ4BUmlkejDRSP+5SIR3\niEthg+FU6GRSQbORE6nhrKjGBk8fpNpozQZVc2VySUTCwHIEEAEIACYFAlRJ\nbc8GCwkIBwMCCRA+tiWe3yHfJAQVCAIKAxYCAQIbAwIeAQAA9J0H/RLR/Uwt\nCakrPKtfeGaNuOI45SRTNxM8TklC6tM28sJSzkX8qKPzvI1PxyLhs/i0/fCQ\n7Z5bU6n41oLuqUt2S9vy+ABlChKAeziOqCHUcMzHOtbKiPkKW88aO687nx+A\nol2XOnMTkVIC+edMUgnKp6tKtZnbO4ea6Cg88TFuli4hLHNXTfCECswuxHOc\nAO1OKDRrCd08iPI5CLNCIV60QnduitE1vF6ehgrH25Vl6LEdd8vPVlTYAvsa\n6ySk2RIrHNLUZZ3iII3MBFL8HyINp/XA1BQP+QbH801uSLq8agxM4iFT9C+O\nD147SawUGhjD5RG7T+YtqItzgA1V9l277EXHwwYEVEltzwEIAJD57uX6bOc4\nTgf3utfL/4hdyoqIMVHkYQOvE27wPsZxX08QsdlaNeGji9Ap2ifIDuckUqn6\nJi9jtZDKtOzdTBm6rnG5nPmkn6BJXPhnecQRP8N0XBISnAGmE4t+bxtts5Wb\nqeMdxJYqMiGqzrLBRJEIDTcg3+QF2Y3RywOqlcXqgG/xX++PsvR1Jiz0rEVP\nTcBc7ytyb/Av7mx1S802HRYGJHOFtVLoPTrtPCvv+DRDK8JzxQW2XSQLlI0M\n9s1tmYhCogYIIqKx9qOTd5mFJ1hJlL6i9xDkvE21qPFASFtww5tiYmUfFaxI\nLwbXPZlQ1I/8fuaUdOxctQ+g40ZgHPcAEQEAAf4JAwgdUg8ubE2BT2DITBD+\nXFgjrnUlQBilbN8/do/36KHuImSPO/GGLzKh4+oXxrvLc5fQLjeO+bzeen4u\nCOCBRO0hG7KpJPhQ6+T02uEF6LegE1sEz5hp6BpKUdPZ1+8799Rylb5kubC5\nIKnLqqpGDbH3hIsmSV3CG/ESkaGMLc/K0ZPt1JRWtUQ9GesXT0v6fdM5GB/L\ncZWFdDoYgZAw5BtymE44knIodfDAYJ4DHnPCh/oilWe1qVTQcNMdtkpBgkuo\nTHecqEmiODQz5EX8pVmS596XsnPO299Lo3TbaHUQo7EC6Au1Au9+b5hC1pDa\nFVCLcproi/Cgch0B/NOCFkVLYmp6BEljRj2dSZRWbO0vgl9kFmJEeiiH41+k\nEAI6PASSKZs3BYLFc2I8mBkcvt90kg4MTBjreuk0uWf1hdH2Rv8zprH4h5Uh\ngjx5nUDX8WXyeLxTU5EBKry+A2DIe0Gm0/waxp6lBlUl+7ra28KYEoHm8Nq/\nN9FCuEhFkFgw6EwUp7jsrFcqBKvmni6jyplm+mJXi3CK+IiNcqub4XPnBI97\nlR19fupB/Y6M7yEaxIM8fTQXmP+x/fe8zRphdo+7o+pJQ3hk5LrrNPK8GEZ6\nDLDOHjZzROhOgBvWtbxRktHk+f5YpuQL+xWd33IV1xYSSHuoAm0Zwt0QJxBs\noFBwJEq1NWM4FxXJBogvzV7KFhl/hXgtvx+GaMv3y8gucj+gE89xVv0XBXjl\n5dy5/PgCI0Id+KAFHyKpJA0N0h8O4xdJoNyIBAwDZ8LHt0vlnLGwcJFR9X7/\nPfWe0PFtC3d7cYY3RopDhnRP7MZs1Wo9nZ4IvlXoEsE2nPkWcns+Wv5Yaewr\ns2ra9ZIK7IIJhqKKgmQtCeiXyFwTq+kfunDnxeCavuWL3HuLKIOZf7P9vXXt\nXgEir9rCwF8EGAEIABMFAlRJbdIJED62JZ7fId8kAhsMAAD+LAf+KT1EpkwH\n0ivTHmYako+6qG6DCtzd3TibWw51cmbY20Ph13NIS/MfBo828S9SXm/sVUzN\n/r7qZgZYfI0/j57tG3BguVGm53qya4bINKyi1RjK6aKo/rrzRkh5ZVD5rVNO\nE2zzvyYAnLUWG9AV1OYDxcgLrXqEMWlqZAo+Wmg7VrTBmdCGs/BPvscNgQRr\n6Gpjgmv9ru6LjRL7vFhEcov/tkBLj+CtaWWFTd1s2vBLOs4rCsD9TT/23vfw\nCnokvvVjKYN5oviy61yhpqF1rWlOsxZ4+2sKW3Pq7JLBtmzsZegTONfcQAf7\nqqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA==\r\n=2wIY\r\n-----END PGP PRIVATE KEY BLOCK-----\r\n", - "Activation": null, - "Primary": 1, - "Token": null, - "Active": 1 - } - ] - }, - { - "ID": "_pm5NXefHCdfqXuHiQ_zcOsfC4rGaCwV4lxuJt5qCmZBh4RiQ0k5iA8wLLaphTWHWAETz-WDqjLDRXNWftciXw==", - "DomainID": "l8vWAXHBQmv0u7OVtPbcqMa4iwQaBqowINSQjPrxAr-Da8fVPKUkUcqAq30_BCxj1X0nW70HQRmAa-rIvzmKUA==", - "Email": "hi@protonmail.dev", - "Send": 1, - "Receive": 0, - "Status": 0, - "Type": 2, - "Order": 2, - "DisplayName": "hi", - "Signature": "hi there", - "HasKeys": 0, - "Keys": [] - } - ] -} diff --git a/pkg/pmapi/testdata/routes/auth/2fa/post_401_bad_password.json b/pkg/pmapi/testdata/routes/auth/2fa/post_401_bad_password.json deleted file mode 100644 index 2b384103..00000000 --- a/pkg/pmapi/testdata/routes/auth/2fa/post_401_bad_password.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Code": 8002, - "Error": "Incorrect login credentials.", - "Details": {} -} diff --git a/pkg/pmapi/testdata/routes/auth/2fa/post_422_bad_password.json b/pkg/pmapi/testdata/routes/auth/2fa/post_422_bad_password.json deleted file mode 100644 index 17330259..00000000 --- a/pkg/pmapi/testdata/routes/auth/2fa/post_422_bad_password.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Code": 8002, - "Error": "Incorrect login credentials. Please try again", - "Details": {} -} diff --git a/pkg/pmapi/testdata/routes/auth/2fa/post_response.json b/pkg/pmapi/testdata/routes/auth/2fa/post_response.json deleted file mode 100644 index 354bb31c..00000000 --- a/pkg/pmapi/testdata/routes/auth/2fa/post_response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Code": 1000, - "Scopes": ["full", "mail", "payments", "reset", "keys"] -} \ No newline at end of file diff --git a/pkg/pmapi/testdata/routes/auth/delete_response.json b/pkg/pmapi/testdata/routes/auth/delete_response.json deleted file mode 100644 index fd8e6930..00000000 --- a/pkg/pmapi/testdata/routes/auth/delete_response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Code": 1000 -} - diff --git a/pkg/pmapi/testdata/routes/auth/enc_priv_key.asc b/pkg/pmapi/testdata/routes/auth/enc_priv_key.asc deleted file mode 100644 index 4c59538e..00000000 --- a/pkg/pmapi/testdata/routes/auth/enc_priv_key.asc +++ /dev/null @@ -1,65 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: OpenPGP.js v0.7.1 -Comment: http://openpgpjs.org - -xcMGBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE -WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39 -vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi -MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5 -c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb -DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB -AAH+CQMIvzcDReuJkc9gnxAkfgmnkBFwRQrqT/4UAPOF8WGVo0uNvDo7Snlk -qWsJS+54+/Xx6Jur/PdBWeEu+6+6GnppYuvsaT0D0nFdFhF6pjng+02IOxfG -qlYXYcW4hRru3BfvJlSvU2LL/Z/ooBnw3T5vqd0eFHKrvabUuwf0x3+K/sru -Fp24rl2PU+bzQlUgKpWzKDmO+0RdKQ6KVCyCDMIXaAkALwNffAvYxI0wnb2y -WAV/bGn1ODnszOYPk3pEMR6kKSxLLaO69kYx4eTERFyJ+1puAxEPCk3Cfeif -yDWi4rU03YB16XH7hQLSFl61SKeIYlkKmkO5Hk1ybi/BhvOGBPVeGGbxWnwI -46G8DfBHW0+uvD5cAQtk2d/q3Ge1I+DIyvuRCcSu0XSBNv/Bkpp4IbAUPBaW -TIvf5p9oxw+AjrMtTtcdSiee1S6CvMMaHhVD7SI6qGA8GqwaXueeLuEXa0Ok -BWlehx8wibMi4a9fLcQZtzJkmGhR1WzXcJfiEg32srILwIzPQYxuFdZZ2elb -gYp/bMEIp4LKhi43IyM6peCDHDzEba8NuOSd0heEqFIm0vlXujMhkyMUvDBv -H0V5On4aMuw/aSEKcAdbazppOru/W1ndyFa5ZHQIC19g72ZaDVyYjPyvNgOV -AFqO4o3IbC5z31zMlTtMbAq2RG9svwUVejn0tmF6UPluTe0U1NuXFpLK6TCH -wqocLz4ecptfJQulpYjClVLgzaYGDuKwQpIwPWg5G/DtKSCGNtEkfqB3aemH -V5xmoYm1v5CQZAEvvsrLA6jxCk9lzqYV8QMivWNXUG+mneIEM35G0HOPzXca -LLyB+N8Zxioc9DPGfdbcxXuVgOKRepbkq4xv1pUpMQ4BUmlkejDRSP+5SIR3 -iEthg+FU6GRSQbORE6nhrKjGBk8fpNpozQZVc2VySUTCwHIEEAEIACYFAlRJ -bc8GCwkIBwMCCRA+tiWe3yHfJAQVCAIKAxYCAQIbAwIeAQAA9J0H/RLR/Uwt -CakrPKtfeGaNuOI45SRTNxM8TklC6tM28sJSzkX8qKPzvI1PxyLhs/i0/fCQ -7Z5bU6n41oLuqUt2S9vy+ABlChKAeziOqCHUcMzHOtbKiPkKW88aO687nx+A -ol2XOnMTkVIC+edMUgnKp6tKtZnbO4ea6Cg88TFuli4hLHNXTfCECswuxHOc -AO1OKDRrCd08iPI5CLNCIV60QnduitE1vF6ehgrH25Vl6LEdd8vPVlTYAvsa -6ySk2RIrHNLUZZ3iII3MBFL8HyINp/XA1BQP+QbH801uSLq8agxM4iFT9C+O -D147SawUGhjD5RG7T+YtqItzgA1V9l277EXHwwYEVEltzwEIAJD57uX6bOc4 -Tgf3utfL/4hdyoqIMVHkYQOvE27wPsZxX08QsdlaNeGji9Ap2ifIDuckUqn6 -Ji9jtZDKtOzdTBm6rnG5nPmkn6BJXPhnecQRP8N0XBISnAGmE4t+bxtts5Wb -qeMdxJYqMiGqzrLBRJEIDTcg3+QF2Y3RywOqlcXqgG/xX++PsvR1Jiz0rEVP -TcBc7ytyb/Av7mx1S802HRYGJHOFtVLoPTrtPCvv+DRDK8JzxQW2XSQLlI0M -9s1tmYhCogYIIqKx9qOTd5mFJ1hJlL6i9xDkvE21qPFASFtww5tiYmUfFaxI -LwbXPZlQ1I/8fuaUdOxctQ+g40ZgHPcAEQEAAf4JAwgdUg8ubE2BT2DITBD+ -XFgjrnUlQBilbN8/do/36KHuImSPO/GGLzKh4+oXxrvLc5fQLjeO+bzeen4u -COCBRO0hG7KpJPhQ6+T02uEF6LegE1sEz5hp6BpKUdPZ1+8799Rylb5kubC5 -IKnLqqpGDbH3hIsmSV3CG/ESkaGMLc/K0ZPt1JRWtUQ9GesXT0v6fdM5GB/L -cZWFdDoYgZAw5BtymE44knIodfDAYJ4DHnPCh/oilWe1qVTQcNMdtkpBgkuo -THecqEmiODQz5EX8pVmS596XsnPO299Lo3TbaHUQo7EC6Au1Au9+b5hC1pDa -FVCLcproi/Cgch0B/NOCFkVLYmp6BEljRj2dSZRWbO0vgl9kFmJEeiiH41+k -EAI6PASSKZs3BYLFc2I8mBkcvt90kg4MTBjreuk0uWf1hdH2Rv8zprH4h5Uh -gjx5nUDX8WXyeLxTU5EBKry+A2DIe0Gm0/waxp6lBlUl+7ra28KYEoHm8Nq/ -N9FCuEhFkFgw6EwUp7jsrFcqBKvmni6jyplm+mJXi3CK+IiNcqub4XPnBI97 -lR19fupB/Y6M7yEaxIM8fTQXmP+x/fe8zRphdo+7o+pJQ3hk5LrrNPK8GEZ6 -DLDOHjZzROhOgBvWtbxRktHk+f5YpuQL+xWd33IV1xYSSHuoAm0Zwt0QJxBs -oFBwJEq1NWM4FxXJBogvzV7KFhl/hXgtvx+GaMv3y8gucj+gE89xVv0XBXjl -5dy5/PgCI0Id+KAFHyKpJA0N0h8O4xdJoNyIBAwDZ8LHt0vlnLGwcJFR9X7/ -PfWe0PFtC3d7cYY3RopDhnRP7MZs1Wo9nZ4IvlXoEsE2nPkWcns+Wv5Yaewr -s2ra9ZIK7IIJhqKKgmQtCeiXyFwTq+kfunDnxeCavuWL3HuLKIOZf7P9vXXt -XgEir9rCwF8EGAEIABMFAlRJbdIJED62JZ7fId8kAhsMAAD+LAf+KT1EpkwH -0ivTHmYako+6qG6DCtzd3TibWw51cmbY20Ph13NIS/MfBo828S9SXm/sVUzN -/r7qZgZYfI0/j57tG3BguVGm53qya4bINKyi1RjK6aKo/rrzRkh5ZVD5rVNO -E2zzvyYAnLUWG9AV1OYDxcgLrXqEMWlqZAo+Wmg7VrTBmdCGs/BPvscNgQRr -6Gpjgmv9ru6LjRL7vFhEcov/tkBLj+CtaWWFTd1s2vBLOs4rCsD9TT/23vfw -CnokvvVjKYN5oviy61yhpqF1rWlOsxZ4+2sKW3Pq7JLBtmzsZegTONfcQAf7 -qqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA== -=2wIY ------END PGP PRIVATE KEY BLOCK----- - - diff --git a/pkg/pmapi/testdata/routes/auth/encrypted_access_token.asc b/pkg/pmapi/testdata/routes/auth/encrypted_access_token.asc deleted file mode 100644 index 756a5053..00000000 --- a/pkg/pmapi/testdata/routes/auth/encrypted_access_token.asc +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN PGP MESSAGE----- -Version: OpenPGP.js v1.2.0 -Comment: http://openpgpjs.org - -wcBMA0fcZ7XLgmf2AQf/RxDfA7g85KzH4371D/jx6deJIXPOWAqgTlGQMsTt -yg4ny3phSC2An/bUXNEBm8UMXqqtS7O+S8n1GjkDrCOkxyC+HugOFQwtybzI -eRX0X0qqvR6ry940SNGjPfJJ4Z0FYSLJtT8YxqO38t38WAYV1j9mBBVPMPJF -r7cQXxEcQAd6NZWF1Cf5Ajuum/zFjbA10Ksbi1tC4fsdtHcS94h1GCfsdNQi -xxbAuoyNYX2wsc6WX8IcmDNn564ZoHfvf2tX4Csf+2czByyOPtfyCn1aee51 -I40/I+65w8NfYEfzu7pbUcdo041Xg3lOhDNcuX/zANNw6zEWbE+12G5KVvwC -NNJgARWnwnOKtov2d73wGqNawn21SzA+zEd2mAPv1LPPIupW+0xOUSp5muov -aLEjcIuZeu+vyhXGZxIgoY4Bw8XCO9uWKZuzmqp+AOIP+kSi5aWnOaDFIOq0 -B3KtZ33bMZeX -=mig5 ------END PGP MESSAGE----- diff --git a/pkg/pmapi/testdata/routes/auth/info/post_response.json b/pkg/pmapi/testdata/routes/auth/info/post_response.json deleted file mode 100644 index 5c637117..00000000 --- a/pkg/pmapi/testdata/routes/auth/info/post_response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Code": 1000, - "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nW2z5HBi8RvsfYzZTS7qBaUxxPhsfHJFZpu3Kd6s1JafNrCCH9rfvPLrfuqocxWPgWDH2R8neK7PkNvjxto9TStuY5z7jAzWRvFWN9cQhAKkdWgy0JY6ywVn22+HFpF4cYesHrqFIKUPDMSSIlWjBVmEJZ/MusD44ZT29xcPrOqeZvwtCffKtGAIjLYPZIEbZKnDM1Dm3q2K/xS5h+xdhjnndhsrkwm9U9oyA2wxzSXFL+pdfj2fOdRwuR5nW0J2NFrq3kJjkRmpO/Genq1UW+TEknIWAb6VzJJJA244K/H8cnSx2+nSNZO3bbo6Ys228ruV9A8m6DhxmS+bihN3ttQ==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j0JEDUFhcTpUY8mAAD8CgEAnsFnF4cF0uSHKkXa1GIa\nGO86yMV4zDZEZcDSJo0fgr8A/AlupGN9EdHlsrZLmTA1vhIx+rOgxdEff28N\nkvNM7qIK\n=q6vu\n-----END PGP SIGNATURE-----\n", - "ServerEphemeral": "5tfigcLKoM0DPWYB+EqYE7QlqsiT63iOVlO5ZX0lTMEILSsrRdVCYrN8L3zkinsAjUZ/cx5wIS7N05k66uZb+ZE3lFOJS2s1BkqLvCrGxYL0e3n5YAnzHYlvCCJKXw/sK57ntfF1OOoblBXX6dw5LjeeDglEep2/DaE0TjD8WUpq4Ls2HlQGn9wrC7dFO2lJXsMhRffxKghiOsdvCLXDmwXginzn/LFezA8KrDsWOBSEGntwpg3s1xFj5h8BqtRHvC0igmoscqgw+3GCMTJ0NZAQ/L+5aJ/0ccL0WBK208ltCNl+/X6Sz0kpyvOP4RqFJhC1auVDJ9AjZQYSYZ1NEQ==", - "Version": 4, - "Salt": "yKlc5/CvObfoiw==", - "SRPSession": "9b2946bbd9055f17c34940abdce0c3d3", - "TwoFactor": 0, - "2FA": { - "TOTP": 1, - "U2F": null - } -} diff --git a/pkg/pmapi/testdata/routes/auth/post_response.json b/pkg/pmapi/testdata/routes/auth/post_response.json deleted file mode 100644 index e64aad83..00000000 --- a/pkg/pmapi/testdata/routes/auth/post_response.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Code": 1000, - "AccessToken": "de0423049b44243afeec7d9c1d99be7b46da1e8a", - "ExpiresIn": 86400, - "TokenType": "Bearer", - "Scopes": ["full", "mail", "payments", "reset", "keys"], - "UID": "729ad6012421d67ad26950dc898bebe3a6e3caa2", - "RefreshToken": "a49b98256745bb497bec20e9b55f5de16f01fb52", - "EventID": "NcKPtU5eMNPMrDkIMbEJrgMtC9yQ7Xc5ZBT-tB3UtV1rZ324RWfCIdBI758q0UnsfywS8CkNenIQlWLIX_dUng==", - "ServerProof": "jZaSvHT6HKSZ2Vl41Q5Po/23KVqEagw1nmgwBDcLLgxzU0QMxVHpZBdiujknpVVV3kAZ9QALiHieoo8yGELpPyrWrIwP38Dw4iT9UL1tprPj2pAhJ3ZsPjQR1peamS1YiJXIbky/FraXEjD50Q/3bSAPP1B2LWJN6s+lrba//Dsp8y6Vp4sEQ2BShrkBTwY3U9/bJ0oaE1Z/j5lN9I6JNmVyFGNW76icU7SfSnYdSiCd//FgkfVtyexYRmNgg9UxbAz7M7wjCyrQGuTVSF5/YzdUp+VxBVosaEh6H0AH4PfP49o85vrgMYBim0ixjm7Eh3xJTuqgbxjzutrS08A4mw==" -} \ No newline at end of file diff --git a/pkg/pmapi/testdata/routes/auth/refresh/post_resp_has_uid.json b/pkg/pmapi/testdata/routes/auth/refresh/post_resp_has_uid.json deleted file mode 100644 index 6d79c4ec..00000000 --- a/pkg/pmapi/testdata/routes/auth/refresh/post_resp_has_uid.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Code": 1000, - "AccessToken": "de0423049b44243afeec7d9c1d99be7b46da1e8a", - "ExpiresIn": 360000, - "TokenType": "Bearer", - "Uid": "729ad6012421d67ad26950dc898bebe3a6e3caa2", - "UID": "729ad6012421d67ad26950dc898bebe3a6e3caa2", - "Scopes": ["full", "mail", "payments", "reset", "keys"], - "RefreshToken": "b894b4c4f20003f12d486900d8b88c7d68e67235" -} diff --git a/pkg/pmapi/testdata/routes/auth/refresh/post_response.json b/pkg/pmapi/testdata/routes/auth/refresh/post_response.json deleted file mode 100644 index 7430374d..00000000 --- a/pkg/pmapi/testdata/routes/auth/refresh/post_response.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Code": 1000, - "AccessToken": "de0423049b44243afeec7d9c1d99be7b46da1e8a", - "ExpiresIn": 360000, - "TokenType": "Bearer", - "Scopes": ["full", "mail", "payments", "reset", "keys"], - "RefreshToken": "b894b4c4f20003f12d486900d8b88c7d68e67235" -} \ No newline at end of file diff --git a/pkg/pmapi/testdata/routes/contacts/put_response.json b/pkg/pmapi/testdata/routes/contacts/put_response.json deleted file mode 100644 index 3dc9da17..00000000 --- a/pkg/pmapi/testdata/routes/contacts/put_response.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "Code": 1000, - "Contact": { - "ID": "l4PrVkmDsIIDba9aln829uwPK0nnyWZHnFtrsyb7CJsYgrD6JTVTuuoaVmaANfO2jIVxzZ2vtbt74rznGjjwFQ==", - "Name": "Bob", - "UID": "proton-web-cd974706-5cde-0e53-e131-c49c88a92ece", - "Size": 303, - "CreateTime": 1517416603, - "ModifyTime": 1517416656, - "ContactEmails": [ - { - "ID": "14n6vuf1zbeo3zsYzgV471S6xJ9gzl7-VZ8tcOTQq6ifBlNEre0SUdUM7sXh6e2Q_4NhJZaU9c7jLdB1HCV6dA==", - "Name": "Bob", - "Email": "bob.changed.tester@protonmail.com", - "Defaults": 1, - "Order": 1, - "ContactID": "l4PrVkmDsIIDba9aln829uwPK0nnyWZHnFtrsyb7CJsYgrD6JTVTuuoaVmaANfO2jIVxzZ2vtbt74rznGjjwFQ==" - } - ] - } -} diff --git a/pkg/pmapi/testdata/routes/keys/salts/get_response.json b/pkg/pmapi/testdata/routes/keys/salts/get_response.json deleted file mode 100644 index 71d29646..00000000 --- a/pkg/pmapi/testdata/routes/keys/salts/get_response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Code": 1000, - "KeySalts": [ - { - "ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==", - "KeySalt": "abc" - }, - { - "ID": "_pm5NXefHCdfqXuHiQ_zcOsfC4rGaCwV4lxuJt5qCmZBh4RiQ0k5iA8wLLaphTWHWAETz-WDqjLDRXNWftciXw==", - "KeySalt": "abc" - } - ] -} diff --git a/pkg/pmapi/testdata/routes/messages/get_response.json b/pkg/pmapi/testdata/routes/messages/get_response.json deleted file mode 100644 index d6b3f184..00000000 --- a/pkg/pmapi/testdata/routes/messages/get_response.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "Code": 1000, - "Message": { - "ID": "AeUizgtA3H44qRgcr-HdBApwLiUhlQg5kB81mg_QalWotmQJIHep9OScWIo7Wu9pnYxM4RqQxJnr3BE4kh4y_Q==", - "Order": 851654, - "ConversationID": "FK4MKKIVJqOC9Pg_sAxCjNWf8PM9yGzrXO3eXq8sk5RJB6HtaRBNUEcnvJBrQVPAtrDSoTNq4Du3FpqIxyMhHQ==", - "Subject": "Welcome to ProtonMail!", - "Unread": 0, - "SenderAddress": "notify@protonmail.com", - "SenderName": "ProtonMail", - "Sender": { - "Address": "notify@protonmail.com", - "Name": "ProtonMail" - }, - "ToList": [ - { - "Address": "apple@protonmail.com", - "Name": "" - } - ], - "Time": 1414098386, - "Size": 4398, - "NumAttachments": 1, - "ExpirationTime": 0, - "Flags": 1, - "SpamScore": 0, - "AddressID": "QMJs2dzTx7uqpH5PNgIzjULywU4gO9uMBhEMVFOAVJOoUml54gC0CCHtW9qYwzH-zYbZwMv3MFYncPjW1Usq7Q==", - "CCList": [], - "BCCList": [], - "ExternalID": null, - "Body": "
jeej saas

Sent from ProtonMail, encrypted email based in Switzerland.

", - "Header": "Content-Description: an awesome email\r\nX-Mailer: CroutonMail\r\n", - "ReplyTo": { - "Address": "notify@protonmail.com", - "Name": "ProtonMail" - }, - "Attachments": [ - { - "ID": "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw==", - "Name": "croutonmail.txt", - "Size": 77, - "MIMEType": "text/plain", - "KeyPackets": "wcBMA0fcZ7XLgmf2AQgAiRsOlnm1kSB4/lr7tYe6pBsRGn10GqwUhrwU5PMKOHdCgnO12jO3y3CzP0Yl/jGhAYja9wLDqH8X0sk3tY32u4Sb1Qe5IuzggAiCa4dwOJj5gEFMTHMzjIMPHR7A70XqUxMhmILye8V4KRm/j4c1sxbzA1rM3lYBumQuB5l/ck0Kgt4ZqxHVXHK5Q1l65FHhSXRj8qnunasHa30TYNzP8nmBA8BinnJxpiQ7FGc2umnUhgkFtjm5ixu9vyjr9ukwDTbwAXXfmY+o7tK7kqIXJcmTL6k2UeC6Mz1AagQtRCRtU+bv/3zGojq/trZo9lom3naIeQYa36Ketmcpj2Qwjg==", - "Headers": { - "content-description": "attachment", - "x-pm-incorporated":"1", - "x-pm-notes": ["You'll never believe", "what's in this text file"] - }, - "MessageID": "AeUizgtA3H44qRgcr-HdBApwLiUhlQg5kB81mg_QalWotmQJIHep9OScWIo7Wu9pnYxM4RqQxJnr3BE4kh4y_Q==" - } - ], - "LabelIDs": [ - "10", - "0" - ] - } -} diff --git a/pkg/pmapi/testdata/routes/messages/label/put_response.json b/pkg/pmapi/testdata/routes/messages/label/put_response.json deleted file mode 100644 index 564fffff..00000000 --- a/pkg/pmapi/testdata/routes/messages/label/put_response.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Code": 1001, - "Responses": [ - { - "ID": "LKJLalkfejl==", - "Response": { - "Code": 1000 - } - }, - { - "ID": "ASia83sJaL==", - "Response": { - "Code": 1000 - } - } - ] -} diff --git a/pkg/pmapi/testdata/routes/users/get_response.json b/pkg/pmapi/testdata/routes/users/get_response.json deleted file mode 100644 index 8c83d331..00000000 --- a/pkg/pmapi/testdata/routes/users/get_response.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "Code": 1000, - "User": { - "ID": "MJLke8kWh1BBvG95JBIrZvzpgsZ94hNNgjNHVyhXMiv4g9cn6SgvqiIFR5cigpml2LD_iUk_3DkV29oojTt3eA==", - "Name": "jason", - "UsedSpace": 23456, - "Currency": "USD", - "Credit": 0, - "MaxSpace": 12345678, - "MaxUpload": 26214400, - "Role": 2, - "Private": 1, - "Subscribed": 1, - "Services": 1, - "Delinquent": 0, - "Keys": [ - { - "ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==", - "Version": 3, - "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: OpenPGP.js v0.7.1\r\nComment: http://openpgpjs.org\r\n\r\nxcMGBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE\nWSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39\nvPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi\nMeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5\nc8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb\nDEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB\nAAH+CQMIvzcDReuJkc9gnxAkfgmnkBFwRQrqT/4UAPOF8WGVo0uNvDo7Snlk\nqWsJS+54+/Xx6Jur/PdBWeEu+6+6GnppYuvsaT0D0nFdFhF6pjng+02IOxfG\nqlYXYcW4hRru3BfvJlSvU2LL/Z/ooBnw3T5vqd0eFHKrvabUuwf0x3+K/sru\nFp24rl2PU+bzQlUgKpWzKDmO+0RdKQ6KVCyCDMIXaAkALwNffAvYxI0wnb2y\nWAV/bGn1ODnszOYPk3pEMR6kKSxLLaO69kYx4eTERFyJ+1puAxEPCk3Cfeif\nyDWi4rU03YB16XH7hQLSFl61SKeIYlkKmkO5Hk1ybi/BhvOGBPVeGGbxWnwI\n46G8DfBHW0+uvD5cAQtk2d/q3Ge1I+DIyvuRCcSu0XSBNv/Bkpp4IbAUPBaW\nTIvf5p9oxw+AjrMtTtcdSiee1S6CvMMaHhVD7SI6qGA8GqwaXueeLuEXa0Ok\nBWlehx8wibMi4a9fLcQZtzJkmGhR1WzXcJfiEg32srILwIzPQYxuFdZZ2elb\ngYp/bMEIp4LKhi43IyM6peCDHDzEba8NuOSd0heEqFIm0vlXujMhkyMUvDBv\nH0V5On4aMuw/aSEKcAdbazppOru/W1ndyFa5ZHQIC19g72ZaDVyYjPyvNgOV\nAFqO4o3IbC5z31zMlTtMbAq2RG9svwUVejn0tmF6UPluTe0U1NuXFpLK6TCH\nwqocLz4ecptfJQulpYjClVLgzaYGDuKwQpIwPWg5G/DtKSCGNtEkfqB3aemH\nV5xmoYm1v5CQZAEvvsrLA6jxCk9lzqYV8QMivWNXUG+mneIEM35G0HOPzXca\nLLyB+N8Zxioc9DPGfdbcxXuVgOKRepbkq4xv1pUpMQ4BUmlkejDRSP+5SIR3\niEthg+FU6GRSQbORE6nhrKjGBk8fpNpozQZVc2VySUTCwHIEEAEIACYFAlRJ\nbc8GCwkIBwMCCRA+tiWe3yHfJAQVCAIKAxYCAQIbAwIeAQAA9J0H/RLR/Uwt\nCakrPKtfeGaNuOI45SRTNxM8TklC6tM28sJSzkX8qKPzvI1PxyLhs/i0/fCQ\n7Z5bU6n41oLuqUt2S9vy+ABlChKAeziOqCHUcMzHOtbKiPkKW88aO687nx+A\nol2XOnMTkVIC+edMUgnKp6tKtZnbO4ea6Cg88TFuli4hLHNXTfCECswuxHOc\nAO1OKDRrCd08iPI5CLNCIV60QnduitE1vF6ehgrH25Vl6LEdd8vPVlTYAvsa\n6ySk2RIrHNLUZZ3iII3MBFL8HyINp/XA1BQP+QbH801uSLq8agxM4iFT9C+O\nD147SawUGhjD5RG7T+YtqItzgA1V9l277EXHwwYEVEltzwEIAJD57uX6bOc4\nTgf3utfL/4hdyoqIMVHkYQOvE27wPsZxX08QsdlaNeGji9Ap2ifIDuckUqn6\nJi9jtZDKtOzdTBm6rnG5nPmkn6BJXPhnecQRP8N0XBISnAGmE4t+bxtts5Wb\nqeMdxJYqMiGqzrLBRJEIDTcg3+QF2Y3RywOqlcXqgG/xX++PsvR1Jiz0rEVP\nTcBc7ytyb/Av7mx1S802HRYGJHOFtVLoPTrtPCvv+DRDK8JzxQW2XSQLlI0M\n9s1tmYhCogYIIqKx9qOTd5mFJ1hJlL6i9xDkvE21qPFASFtww5tiYmUfFaxI\nLwbXPZlQ1I/8fuaUdOxctQ+g40ZgHPcAEQEAAf4JAwgdUg8ubE2BT2DITBD+\nXFgjrnUlQBilbN8/do/36KHuImSPO/GGLzKh4+oXxrvLc5fQLjeO+bzeen4u\nCOCBRO0hG7KpJPhQ6+T02uEF6LegE1sEz5hp6BpKUdPZ1+8799Rylb5kubC5\nIKnLqqpGDbH3hIsmSV3CG/ESkaGMLc/K0ZPt1JRWtUQ9GesXT0v6fdM5GB/L\ncZWFdDoYgZAw5BtymE44knIodfDAYJ4DHnPCh/oilWe1qVTQcNMdtkpBgkuo\nTHecqEmiODQz5EX8pVmS596XsnPO299Lo3TbaHUQo7EC6Au1Au9+b5hC1pDa\nFVCLcproi/Cgch0B/NOCFkVLYmp6BEljRj2dSZRWbO0vgl9kFmJEeiiH41+k\nEAI6PASSKZs3BYLFc2I8mBkcvt90kg4MTBjreuk0uWf1hdH2Rv8zprH4h5Uh\ngjx5nUDX8WXyeLxTU5EBKry+A2DIe0Gm0/waxp6lBlUl+7ra28KYEoHm8Nq/\nN9FCuEhFkFgw6EwUp7jsrFcqBKvmni6jyplm+mJXi3CK+IiNcqub4XPnBI97\nlR19fupB/Y6M7yEaxIM8fTQXmP+x/fe8zRphdo+7o+pJQ3hk5LrrNPK8GEZ6\nDLDOHjZzROhOgBvWtbxRktHk+f5YpuQL+xWd33IV1xYSSHuoAm0Zwt0QJxBs\noFBwJEq1NWM4FxXJBogvzV7KFhl/hXgtvx+GaMv3y8gucj+gE89xVv0XBXjl\n5dy5/PgCI0Id+KAFHyKpJA0N0h8O4xdJoNyIBAwDZ8LHt0vlnLGwcJFR9X7/\nPfWe0PFtC3d7cYY3RopDhnRP7MZs1Wo9nZ4IvlXoEsE2nPkWcns+Wv5Yaewr\ns2ra9ZIK7IIJhqKKgmQtCeiXyFwTq+kfunDnxeCavuWL3HuLKIOZf7P9vXXt\nXgEir9rCwF8EGAEIABMFAlRJbdIJED62JZ7fId8kAhsMAAD+LAf+KT1EpkwH\n0ivTHmYako+6qG6DCtzd3TibWw51cmbY20Ph13NIS/MfBo828S9SXm/sVUzN\n/r7qZgZYfI0/j57tG3BguVGm53qya4bINKyi1RjK6aKo/rrzRkh5ZVD5rVNO\nE2zzvyYAnLUWG9AV1OYDxcgLrXqEMWlqZAo+Wmg7VrTBmdCGs/BPvscNgQRr\n6Gpjgmv9ru6LjRL7vFhEcov/tkBLj+CtaWWFTd1s2vBLOs4rCsD9TT/23vfw\nCnokvvVjKYN5oviy61yhpqF1rWlOsxZ4+2sKW3Pq7JLBtmzsZegTONfcQAf7\nqqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA==\r\n=2wIY\r\n-----END PGP PRIVATE KEY BLOCK-----\r\n", - "Fingerprint": "c93f767df53b0ca8395cfde90483475164ec6353", - "Activation": null, - "Primary": 1, - "Active": 1 - } - ] - } -} diff --git a/pkg/pmapi/testdata/symmetric_key.json b/pkg/pmapi/testdata/symmetric_key.json deleted file mode 100644 index 7c25796b..00000000 --- a/pkg/pmapi/testdata/symmetric_key.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Key": "ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=", - "Algo": "aes256" -} diff --git a/pkg/pmapi/testdata/testPrivateKey b/pkg/pmapi/testdata/testPrivateKey deleted file mode 100644 index b8df89f9..00000000 --- a/pkg/pmapi/testdata/testPrivateKey +++ /dev/null @@ -1,63 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: OpenPGP.js v0.7.1 -Comment: http://openpgpjs.org - -xcMGBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE -WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39 -vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi -MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5 -c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb -DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB -AAH+CQMIvzcDReuJkc9gnxAkfgmnkBFwRQrqT/4UAPOF8WGVo0uNvDo7Snlk -qWsJS+54+/Xx6Jur/PdBWeEu+6+6GnppYuvsaT0D0nFdFhF6pjng+02IOxfG -qlYXYcW4hRru3BfvJlSvU2LL/Z/ooBnw3T5vqd0eFHKrvabUuwf0x3+K/sru -Fp24rl2PU+bzQlUgKpWzKDmO+0RdKQ6KVCyCDMIXaAkALwNffAvYxI0wnb2y -WAV/bGn1ODnszOYPk3pEMR6kKSxLLaO69kYx4eTERFyJ+1puAxEPCk3Cfeif -yDWi4rU03YB16XH7hQLSFl61SKeIYlkKmkO5Hk1ybi/BhvOGBPVeGGbxWnwI -46G8DfBHW0+uvD5cAQtk2d/q3Ge1I+DIyvuRCcSu0XSBNv/Bkpp4IbAUPBaW -TIvf5p9oxw+AjrMtTtcdSiee1S6CvMMaHhVD7SI6qGA8GqwaXueeLuEXa0Ok -BWlehx8wibMi4a9fLcQZtzJkmGhR1WzXcJfiEg32srILwIzPQYxuFdZZ2elb -gYp/bMEIp4LKhi43IyM6peCDHDzEba8NuOSd0heEqFIm0vlXujMhkyMUvDBv -H0V5On4aMuw/aSEKcAdbazppOru/W1ndyFa5ZHQIC19g72ZaDVyYjPyvNgOV -AFqO4o3IbC5z31zMlTtMbAq2RG9svwUVejn0tmF6UPluTe0U1NuXFpLK6TCH -wqocLz4ecptfJQulpYjClVLgzaYGDuKwQpIwPWg5G/DtKSCGNtEkfqB3aemH -V5xmoYm1v5CQZAEvvsrLA6jxCk9lzqYV8QMivWNXUG+mneIEM35G0HOPzXca -LLyB+N8Zxioc9DPGfdbcxXuVgOKRepbkq4xv1pUpMQ4BUmlkejDRSP+5SIR3 -iEthg+FU6GRSQbORE6nhrKjGBk8fpNpozQZVc2VySUTCwHIEEAEIACYFAlRJ -bc8GCwkIBwMCCRA+tiWe3yHfJAQVCAIKAxYCAQIbAwIeAQAA9J0H/RLR/Uwt -CakrPKtfeGaNuOI45SRTNxM8TklC6tM28sJSzkX8qKPzvI1PxyLhs/i0/fCQ -7Z5bU6n41oLuqUt2S9vy+ABlChKAeziOqCHUcMzHOtbKiPkKW88aO687nx+A -ol2XOnMTkVIC+edMUgnKp6tKtZnbO4ea6Cg88TFuli4hLHNXTfCECswuxHOc -AO1OKDRrCd08iPI5CLNCIV60QnduitE1vF6ehgrH25Vl6LEdd8vPVlTYAvsa -6ySk2RIrHNLUZZ3iII3MBFL8HyINp/XA1BQP+QbH801uSLq8agxM4iFT9C+O -D147SawUGhjD5RG7T+YtqItzgA1V9l277EXHwwYEVEltzwEIAJD57uX6bOc4 -Tgf3utfL/4hdyoqIMVHkYQOvE27wPsZxX08QsdlaNeGji9Ap2ifIDuckUqn6 -Ji9jtZDKtOzdTBm6rnG5nPmkn6BJXPhnecQRP8N0XBISnAGmE4t+bxtts5Wb -qeMdxJYqMiGqzrLBRJEIDTcg3+QF2Y3RywOqlcXqgG/xX++PsvR1Jiz0rEVP -TcBc7ytyb/Av7mx1S802HRYGJHOFtVLoPTrtPCvv+DRDK8JzxQW2XSQLlI0M -9s1tmYhCogYIIqKx9qOTd5mFJ1hJlL6i9xDkvE21qPFASFtww5tiYmUfFaxI -LwbXPZlQ1I/8fuaUdOxctQ+g40ZgHPcAEQEAAf4JAwgdUg8ubE2BT2DITBD+ -XFgjrnUlQBilbN8/do/36KHuImSPO/GGLzKh4+oXxrvLc5fQLjeO+bzeen4u -COCBRO0hG7KpJPhQ6+T02uEF6LegE1sEz5hp6BpKUdPZ1+8799Rylb5kubC5 -IKnLqqpGDbH3hIsmSV3CG/ESkaGMLc/K0ZPt1JRWtUQ9GesXT0v6fdM5GB/L -cZWFdDoYgZAw5BtymE44knIodfDAYJ4DHnPCh/oilWe1qVTQcNMdtkpBgkuo -THecqEmiODQz5EX8pVmS596XsnPO299Lo3TbaHUQo7EC6Au1Au9+b5hC1pDa -FVCLcproi/Cgch0B/NOCFkVLYmp6BEljRj2dSZRWbO0vgl9kFmJEeiiH41+k -EAI6PASSKZs3BYLFc2I8mBkcvt90kg4MTBjreuk0uWf1hdH2Rv8zprH4h5Uh -gjx5nUDX8WXyeLxTU5EBKry+A2DIe0Gm0/waxp6lBlUl+7ra28KYEoHm8Nq/ -N9FCuEhFkFgw6EwUp7jsrFcqBKvmni6jyplm+mJXi3CK+IiNcqub4XPnBI97 -lR19fupB/Y6M7yEaxIM8fTQXmP+x/fe8zRphdo+7o+pJQ3hk5LrrNPK8GEZ6 -DLDOHjZzROhOgBvWtbxRktHk+f5YpuQL+xWd33IV1xYSSHuoAm0Zwt0QJxBs -oFBwJEq1NWM4FxXJBogvzV7KFhl/hXgtvx+GaMv3y8gucj+gE89xVv0XBXjl -5dy5/PgCI0Id+KAFHyKpJA0N0h8O4xdJoNyIBAwDZ8LHt0vlnLGwcJFR9X7/ -PfWe0PFtC3d7cYY3RopDhnRP7MZs1Wo9nZ4IvlXoEsE2nPkWcns+Wv5Yaewr -s2ra9ZIK7IIJhqKKgmQtCeiXyFwTq+kfunDnxeCavuWL3HuLKIOZf7P9vXXt -XgEir9rCwF8EGAEIABMFAlRJbdIJED62JZ7fId8kAhsMAAD+LAf+KT1EpkwH -0ivTHmYako+6qG6DCtzd3TibWw51cmbY20Ph13NIS/MfBo828S9SXm/sVUzN -/r7qZgZYfI0/j57tG3BguVGm53qya4bINKyi1RjK6aKo/rrzRkh5ZVD5rVNO -E2zzvyYAnLUWG9AV1OYDxcgLrXqEMWlqZAo+Wmg7VrTBmdCGs/BPvscNgQRr -6Gpjgmv9ru6LjRL7vFhEcov/tkBLj+CtaWWFTd1s2vBLOs4rCsD9TT/23vfw -CnokvvVjKYN5oviy61yhpqF1rWlOsxZ4+2sKW3Pq7JLBtmzsZegTONfcQAf7 -qqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA== -=2wIY ------END PGP PRIVATE KEY BLOCK----- diff --git a/pkg/pmapi/testdata/testPrivateKeyLegacy b/pkg/pmapi/testdata/testPrivateKeyLegacy deleted file mode 100644 index e058468d..00000000 --- a/pkg/pmapi/testdata/testPrivateKeyLegacy +++ /dev/null @@ -1,63 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: OpenPGP.js v0.9.0 -Comment: http://openpgpjs.org - -xcMGBFSjdRkBB/9slBPGNrHAMbYT71AnxF4a0W/fcrzCP27yd1nte+iUKGyh -yux3xGQRIHrwB9zyYBPFORXXwaQIA3YDH73YnE0FPfjh+fBWENWXKBkOVx1R -efPTytGIyATFtLvmN1D65WkvnIfBdcOc7FWj6N4w5yOajpL3u/46Pe73ypic -he10XuwO4198q/8YamGpTFgQVj4H7QbtuIxoV+umIAf96p9PCMAxipF+piao -D8LYWDUCK/wr1tSXIkNKL+ZCyuCYyIAnOli7xgIlKNCWvC8csuJEYcZlmf42 -/iHyrWeusyumLeBPhRABikE2ePSo+XI7LznD/CIrLhEk6RJT31+JR0NlABEB -AAH+CQMIGhfYEFuRjVpgaSOmgLetjNJyo++e3P3RykGb5AL/vo5LUzlGX95c -gQWSNyYYBo7xzDw8K02dGF4y9Hq6zQDFkA9jOI2XX/qq4GYb7K515aJZwnuF -wQ+SntabFrdty8oV33Ufm8Y/TSUP/swbOP6xlXIk8Gy06D8JHW22oN35Lcww -LftEo5Y0rD+OFlZWnA9fe/Q6CO4OGn5DJs0HbQIlNPU1sK3i0dEjCgDJq0Fx -6WczXpB16jLiNh0W3X/HsjgSKT7Zm3nSPW6Y5mK3y7dnlfHt+A8F1ONYbpNt -RzaoiIaKm3hoFKyAP4vAkto1IaCfZRyVr5TQQh2UJO9S/o5dCEUNw2zXhF+Z -O3QQfFZgQjyEPgbzVmsc/zfNUyB4PEPEOMO/9IregXa/Ij42dIEoczKQzlR0 -mHCNReLfu/B+lVNj0xMrodx9slCpH6qWMKGQ7dR4eLU2+2BZvK0UeG/QY2xe -IvLLLptm0IBbfnWYZOWSFnqaT5NMN0idMlLBCYQoOtpgmd4voND3xpBXmTIv -O5t4CTqK/KO8+lnL75e5X2ygZ+f1x6tPa/B45C4w+TtgITXZMlp7OE8RttO6 -v+0Fg6vGAmqHJzGckCYhwvxRJoyndRd501a/W6PdImZQJ5bPYYlaFiaF+Vxx -ovNb7AvUsDfknr80IdzxanKq3TFf+vCmNWs9tjXgZe0POwFZvjTdErf+lZcz -p4lTMipdA7zYksoNobNODjBgMwm5H5qMCYDothG9EF1dU/u/MOrCcgIPFouL -Z/MiY665T9xjLOHm1Hed8LI1Fkzoclkh2yRwdFDtbFGTSq00LDcDwuluRM/8 -J6hCQQ72OT7SBtbCVhljbPbzLCuvZ8mDscvardQkYI6x7g4QhKLNQVyVk1nA -N4g59mSICpixvgihiFZbuxYjYxoWJMJvzQZVc2VySUTCwHIEEAEIACYFAlSj -dSQGCwkIBwMCCRB9LVPeS8+0BAQVCAIKAxYCAQIbAwIeAQAAFwoH/ArDQgdL -SnS68BnvnQy0xhnYMmK99yc+hlbWuiTJeK3HH+U/EIkT5DiFiEyE6YuZmsa5 -9cO8jlCN8ZKgiwhDvb6i4SEa9f2gar1VCPtC+4KCaFa8esp0kdSjTRzP4ZLb -QPrdbfPeKoLoOoaKFH8bRVlPCnrCioHTBTsbLdzg03mcczusZomn/TKH/8tT -OctX7CrlB+ewCUc5CWL4mZqRFjAMSJpogj7/4jEVHke4V/frKRtjvQNDcuOo -PPU+fVpHq4ILuv7pYF9DujAIbLgWN/tdE4Goxsrm+aCUyylQ2P55Vb5mhAPu -CLYXqSELPi99/NKEM9xhLa/1HwdTwQ/1X0zHwwYEVKN1JAEH/3XCsZ/W7fnw -zMbkE+rMUlo1+KbX+ltEG7nAwP+Q8NrwhbwhmpA3bHM3bhSdt0CO4mRx4oOR -cqeTNjFftQzPxCbPTmcTCupNCODOK4rnEn9i9lz7/JtkOf55+/oHbx+pjvDz -rA7u+ugNHzDYTd+nh2ue99HWoSZSEWD/sDrp1JEN8M0zxODGYfO/Hgr5Gnnp -TEzDzZ0LvTjYMVcmjvBhtPTNLiQsVakOj1wTLWEgcna2FLHAHh0K63snxAjT -6G1oF0Wn08H7ZP5/WhiMy1Yr+M6N+hsLpOycwtwBdjwDcWLrOhAAj3JMLI6W -zFS6SKUr4wxnZWIPQT7TZNBXeKmbds8AEQEAAf4JAwhPB3Ux5u4eB2CqeaWy -KsvSTH/D1o2QpWujempJ5KtCVstyV4bF1JZ3tadOGOuOpNT7jgcp/Et2VVGs -nHPtws9uStvbY8XcZYuu+BXYEM9tkDbAaanS7FOvh48F8Qa07IQB6JbrpOAW -uQPKtBMEsmBqpyWMPIo856ai1Lwp6ZYovdI/WxHdkcQMg8Jvsi2DFY827/ha -75vTnyDx0psbCUN+kc9rXqwGJlGiBdWmLSGW1cb9Gy05KcAihQmXmp9YaP9y -PMFPHiHMOLn6HPW1xEV8B1jHVF/BfaLDJYSm1q3aDC9/QkV5WLeU7DIzFWN9 -JcMsKwoRJwEf63O3/CZ39RHd9qwFrd+HPIlc7X5Pxop16G1xXAOnLBucup90 -kYwDcbNvyC8TKESf+Ga+Py5If01WhgldBm+wgOZvXnn8SoLO98qAotei8MBi -kI/B+7cqynWg4aoZZP2wOm/dl0zlsXGhoKut2Hxr9BzG/WdbjFRgbWSOMawo -yF5LThbevNLZeLXFcT95NSI2HO2XgNi4I0kqjldY5k9JH0fqUnlQw87CMbVs -TUS78q6IxtljUXJ360kfQh5ue7cRdCPrfWqNyg1YU3s7CXvEfrHNMugES6/N -zAQllWz6MHbbTxFz80l5gi3AJAoB0jQuZsLrm4RB82lmmBuWrQZh4MPtzLg0 -HOGixprygBjuaNUPHT281Ghe2UNPpqlUp8BFkUuHYPe4LWSB2ILNGaWB+nX+ -xmvZMSnI4kVsA8oXOAbg+v5W0sYNIBU4h3nk1KOGHR4kL8fSgDi81dfqtcop -2jzolo0yPMvcrfWnwMaEH/doS3dVBQyrC61si/U6CXLqCS/w+8JTWShVT/6B -NihnIf1ulAhSqoa317/VuYYr7hLTqS+D7O0uMfJ/1SL6/AEy4D1Rc7l8Bd5F -ud9UVvXCwF8EGAEIABMFAlSjdSYJEH0tU95Lz7QEAhsMAACDNwf/WTKH7bS1 -xQYxGtPdqR+FW/ejh30LiPQlrs9AwrBk2JJ0VJtDxkT3FtHlwoH9nfd6YzD7 -ngJ4mxqePuU5559GqgdTKemKsA2C48uanxJbgOivivBI6ziB87W23PDv7wwh -4Ubynw5DkH4nf4oJR2K4H7rN3EZbesh8D04A9gA5tBQnuq5L+Wag2s7MpWYl -ZrvHh/1xLZaWz++3+N4SfaPTH8ao3Qojw/Y+OLGIFjk6B/oVEe9ZZQPhJjHx -gd/qu8VcYdbe10xFFvbiaI/RS6Fs7JRSJCbXE0h7Z8n4hQIP1y6aBZsZeh8a -PPekG4ttm6z3/BqqVplanIRSXlsqyp6J8A== -=Pyb1 ------END PGP PRIVATE KEY BLOCK----- diff --git a/pkg/pmapi/testdata/testPublicKey b/pkg/pmapi/testdata/testPublicKey deleted file mode 100644 index cd70b249..00000000 --- a/pkg/pmapi/testdata/testPublicKey +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: OpenPGP.js v0.7.1 -Comment: http://openpgpjs.org - -xsBNBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE -WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39 -vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi -MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5 -c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb -DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB -AAHNBlVzZXJJRMLAcgQQAQgAJgUCVEltzwYLCQgHAwIJED62JZ7fId8kBBUI -AgoDFgIBAhsDAh4BAAD0nQf9EtH9TC0JqSs8q194Zo244jjlJFM3EzxOSULq -0zbywlLORfyoo/O8jU/HIuGz+LT98JDtnltTqfjWgu6pS3ZL2/L4AGUKEoB7 -OI6oIdRwzMc61sqI+Qpbzxo7rzufH4CiXZc6cxORUgL550xSCcqnq0q1mds7 -h5roKDzxMW6WLiEsc1dN8IQKzC7Ec5wA7U4oNGsJ3TyI8jkIs0IhXrRCd26K -0TW8Xp6GCsfblWXosR13y89WVNgC+xrrJKTZEisc0tRlneIgjcwEUvwfIg2n -9cDUFA/5BsfzTW5IurxqDEziIVP0L44PXjtJrBQaGMPlEbtP5i2oi3OADVX2 -XbvsRc7ATQRUSW3PAQgAkPnu5fps5zhOB/e618v/iF3KiogxUeRhA68TbvA+ -xnFfTxCx2Vo14aOL0CnaJ8gO5yRSqfomL2O1kMq07N1MGbqucbmc+aSfoElc -+Gd5xBE/w3RcEhKcAaYTi35vG22zlZup4x3ElioyIarOssFEkQgNNyDf5AXZ -jdHLA6qVxeqAb/Ff74+y9HUmLPSsRU9NwFzvK3Jv8C/ubHVLzTYdFgYkc4W1 -Uug9Ou08K+/4NEMrwnPFBbZdJAuUjQz2zW2ZiEKiBggiorH2o5N3mYUnWEmU -vqL3EOS8TbWo8UBIW3DDm2JiZR8VrEgvBtc9mVDUj/x+5pR07Fy1D6DjRmAc -9wARAQABwsBfBBgBCAATBQJUSW3SCRA+tiWe3yHfJAIbDAAA/iwH/ik9RKZM -B9Ir0x5mGpKPuqhugwrc3d04m1sOdXJm2NtD4ddzSEvzHwaPNvEvUl5v7FVM -zf6+6mYGWHyNP4+e7RtwYLlRpud6smuGyDSsotUYyumiqP6680ZIeWVQ+a1T -ThNs878mAJy1FhvQFdTmA8XIC616hDFpamQKPlpoO1a0wZnQhrPwT77HDYEE -a+hqY4Jr/a7ui40S+7xYRHKL/7ZAS4/grWllhU3dbNrwSzrOKwrA/U0/9t73 -8Ap6JL71YymDeaL4sutcoaahda1pTrMWePtrCltz6uySwbZs7GXoEzjX3EAH -+6qhkUJtzMaE3YEFEoQMGzcDTUEfXCJ3zJw= -=yT9U ------END PGP PUBLIC KEY BLOCK----- - diff --git a/pkg/pmapi/users.go b/pkg/pmapi/users.go deleted file mode 100644 index 161f4218..00000000 --- a/pkg/pmapi/users.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/go-resty/resty/v2" - "github.com/pkg/errors" -) - -// Role values. -const ( - FreeUserRole = iota - PaidMemberRole - PaidAdminRole -) - -// User status. -const ( - DeletedUser = 0 - DisabledUser = 1 - ActiveUser = 2 - VPNAdminUser = 3 - AdminUser = 4 - SuperUser = 5 -) - -// Delinquent values. -const ( - CurrentUser = iota - AvailableUser - OverdueUser - DelinquentUser - NoReceiveUser -) - -// PMSignature values. -const ( - PMSignatureDisabled = iota - PMSignatureEnabled - PMSignatureLocked -) - -// User holds the user details. -type User struct { - ID string - Name string - UsedSpace *int64 - Currency string - Credit int - MaxSpace *int64 - MaxUpload int64 - Role int - Private int - Subscribed int - Services int - Deliquent int - - Keys PMKeys - - VPN struct { - Status int - ExpirationTime int - PlanName string - MaxConnect int - MaxTier int - } -} - -func (c *client) GetUser(ctx context.Context) (user *User, err error) { - var res struct { - User *User - } - - if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { - return r.SetResult(&res).Get("/users") - }); err != nil { - return nil, err - } - - return res.User, nil -} - -// unlockUser unlocks all the client's user keys using the given passphrase. -func (c *client) unlockUser(passphrase []byte) (err error) { - if c.user == nil { - return errors.New("user data is not loaded") - } - - if c.userKeyRing, err = c.user.Keys.UnlockAll(passphrase, nil); err != nil { - return errors.Wrap(err, "failed to unlock user keys") - } - - return -} - -// UpdateUser retrieves details about user and loads its addresses. -func (c *client) UpdateUser(ctx context.Context) (*User, error) { - user, err := c.GetUser(ctx) - if err != nil { - return nil, err - } - - addresses, err := c.GetAddresses(ctx) - if err != nil { - return nil, err - } - - c.user = user - c.addresses = addresses - c.manager.setSentryUserID(user.ID) - - return user, err -} - -// CurrentUser returns currently active user or user will be updated. -func (c *client) CurrentUser(ctx context.Context) (*User, error) { - if c.user != nil && len(c.addresses) != 0 { - return c.user, nil - } - - return c.UpdateUser(ctx) -} - -// CurrentUser returns currently active user or user will be updated. -func (c *client) GetUserKeyRing() (*crypto.KeyRing, error) { - if c.userKeyRing == nil { - return nil, errors.New("user keyring is not available") - } - - return c.userKeyRing, nil -} diff --git a/pkg/pmapi/users_test.go b/pkg/pmapi/users_test.go deleted file mode 100644 index 750c9850..00000000 --- a/pkg/pmapi/users_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -import ( - "context" - "net/http" - "testing" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - r "github.com/stretchr/testify/require" -) - -var ( - usedSpace = int64(23456) - maxSpace = int64(12345678) - - testCurrentUser = &User{ - ID: "MJLke8kWh1BBvG95JBIrZvzpgsZ94hNNgjNHVyhXMiv4g9cn6SgvqiIFR5cigpml2LD_iUk_3DkV29oojTt3eA==", - Name: "jason", - UsedSpace: &usedSpace, - Currency: "USD", - Role: 2, - Subscribed: 1, - Services: 1, - MaxSpace: &maxSpace, - MaxUpload: 26214400, - Private: 1, - Keys: *loadPMKeys(readTestFile("keyring_userKey_JSON", false)), - } -) - -func routeGetUsers(tb testing.TB, w http.ResponseWriter, req *http.Request) string { - r.NoError(tb, checkMethodAndPath(req, "GET", "/users")) - r.NoError(tb, isAuthReq(req, testUID, testAccessToken)) - return "users/get_response.json" -} - -func TestClient_CurrentUser(t *testing.T) { - finish, c := newTestClientCallbacks(t, - routeGetUsers, - routeGetAddresses, - ) - defer finish() - - user, err := c.CurrentUser(context.Background()) - r.Nil(t, err) - - // Ignore KeyRings during the check because they have unexported fields and cannot be compared - r.True(t, cmp.Equal(user, testCurrentUser, cmpopts.IgnoreTypes(&crypto.Key{}))) - - r.NoError(t, c.Unlock(context.Background(), []byte(testMailboxPassword))) -} diff --git a/pkg/pmapi/utils.go b/pkg/pmapi/utils.go deleted file mode 100644 index e6c9adfc..00000000 --- a/pkg/pmapi/utils.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pmapi - -func iHasFlag(i, flag int) bool { return i&flag == flag } -func iHasAtLeastOneFlag(i, flag int) bool { return i&flag > 0 } -func iIsFlag(i, flag int) bool { return i == flag } -func iHasNoneOfFlag(i, flag int) bool { return !iHasAtLeastOneFlag(i, flag) } diff --git a/pkg/pool/pool.go b/pkg/pool/pool.go deleted file mode 100644 index f979a370..00000000 --- a/pkg/pool/pool.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pool - -import ( - "sync" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pchan" -) - -type WorkFunc func(interface{}, int) (interface{}, error) - -type DoneFunc func() - -type Pool struct { - jobCh *pchan.PChan -} - -func New(size int, work WorkFunc) *Pool { - jobCh := pchan.New() - - for i := 0; i < size; i++ { - go func() { - for { - val, prio, ok := jobCh.Pop() - if !ok { - return - } - - job, ok := val.(*Job) - if !ok { - panic("bad result type") - } - - res, err := work(job.req, prio) - if err != nil { - job.postFailure(err) - } else { - job.postSuccess(res) - } - - job.waitDone() - } - }() - } - - return &Pool{jobCh: jobCh} -} - -func (pool *Pool) NewJob(req interface{}, prio int) (*Job, DoneFunc) { - job := newJob(req) - - job.setItem(pool.jobCh.Push(job, prio)) - - return job, job.markDone -} - -type Job struct { - req interface{} - res interface{} - err error - - item *pchan.Item - - ready, done sync.WaitGroup - once sync.Once -} - -func newJob(req interface{}) *Job { - job := &Job{req: req} - - job.ready.Add(1) - job.done.Add(1) - - return job -} - -func (job *Job) GetResult() (interface{}, error) { - job.ready.Wait() - - return job.res, job.err -} - -func (job *Job) GetPriority() int { - return job.item.GetPriority() -} - -func (job *Job) SetPriority(prio int) { - job.item.SetPriority(prio) -} - -func (job *Job) postSuccess(res interface{}) { - defer job.ready.Done() - - job.res = res -} - -func (job *Job) postFailure(err error) { - defer job.ready.Done() - - job.err = err -} - -func (job *Job) setItem(item *pchan.Item) { - job.item = item -} - -func (job *Job) markDone() { - job.once.Do(func() { job.done.Done() }) -} - -func (job *Job) waitDone() { - job.done.Wait() -} diff --git a/pkg/pool/pool_test.go b/pkg/pool/pool_test.go deleted file mode 100644 index 27e8137b..00000000 --- a/pkg/pool/pool_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package pool_test - -import ( - "testing" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pool" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPool(t *testing.T) { - pool := pool.New(2, func(req interface{}, prio int) (interface{}, error) { return req, nil }) - - job1, done1 := pool.NewJob("echo", 1) - defer done1() - - job2, done2 := pool.NewJob("this", 1) - defer done2() - - res2, err := job2.GetResult() - require.NoError(t, err) - - res1, err := job1.GetResult() - require.NoError(t, err) - - assert.Equal(t, "echo", res1) - assert.Equal(t, "this", res2) -} diff --git a/pkg/ports/ports.go b/pkg/ports/ports.go index 5b75ba8b..c3acfd65 100644 --- a/pkg/ports/ports.go +++ b/pkg/ports/ports.go @@ -20,6 +20,8 @@ package ports import ( "fmt" "net" + + "github.com/ProtonMail/proton-bridge/v2/internal/constants" ) const ( @@ -32,7 +34,7 @@ func IsPortFree(port int) bool { return false } // First, check localhost only. - if isOccupied(fmt.Sprintf("127.0.0.1:%d", port)) { + if isOccupied(fmt.Sprintf("%v:%d", constants.Host, port)) { return false } // Second, check also ports opened to public. diff --git a/pkg/restarter/restarter.go b/pkg/restarter/restarter.go new file mode 100644 index 00000000..0745bccd --- /dev/null +++ b/pkg/restarter/restarter.go @@ -0,0 +1,99 @@ +package restarter + +import ( + "os" + "strconv" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/sys/execabs" +) + +const ( + BridgeCrashCount = "BRIDGE_CRASH_COUNT" + BridgeLauncher = "BRIDGE_LAUNCHER" +) + +type Restarter struct { + restart bool + crash bool + exe string +} + +func New() *Restarter { + var exe string + + if osExe, err := os.Executable(); err == nil { + exe = osExe + } else { + logrus.WithError(err).Error("Failed to get executable path, the app will not be able to restart") + } + + return &Restarter{exe: exe} +} + +func (restarter *Restarter) Set(restart, crash bool) { + restarter.restart = restart + restarter.crash = crash +} + +func (restarter *Restarter) Restart() { + if !restarter.restart { + return + } + + if restarter.exe == "" { + return + } + + env := getEnvMap() + + if restarter.crash { + env[BridgeCrashCount] = increment(env[BridgeLauncher]) + } else { + delete(env, BridgeCrashCount) + } + + cmd := execabs.Command(restarter.exe, os.Args[1:]...) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = getEnvList(env) + + if err := run(cmd); err != nil { + logrus.WithError(err).Error("Failed to restart") + } +} + +func getEnvMap() map[string]string { + env := make(map[string]string) + + for _, entry := range os.Environ() { + if split := strings.SplitN(entry, "=", 2); len(split) == 2 { + env[split[0]] = split[1] + } + } + + return env +} + +func getEnvList(envMap map[string]string) []string { + env := make([]string, 0, len(envMap)) + + for key, value := range envMap { + env = append(env, key+"="+value) + } + + return env +} + +func increment(value string) string { + var valueInt int + + if parsed, err := strconv.Atoi(value); err == nil { + valueInt = parsed + } + + return strconv.Itoa(valueInt + 1) +} diff --git a/pkg/restarter/start_default.go b/pkg/restarter/start_default.go new file mode 100644 index 00000000..3aa14ee5 --- /dev/null +++ b/pkg/restarter/start_default.go @@ -0,0 +1,7 @@ +package restarter + +import "os/exec" + +func run(cmd *exec.Cmd) error { + return cmd.Start() +} diff --git a/pkg/restarter/start_windows.go b/pkg/restarter/start_windows.go new file mode 100644 index 00000000..781c72f0 --- /dev/null +++ b/pkg/restarter/start_windows.go @@ -0,0 +1,7 @@ +package restarter + +import "os/exec" + +func run(cmd *exec.Cmd) error { + return cmd.Run() +} diff --git a/pkg/semaphore/semaphore.go b/pkg/semaphore/semaphore.go deleted file mode 100644 index 4d5e3343..00000000 --- a/pkg/semaphore/semaphore.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package semaphore - -import "sync" - -type Semaphore struct { - ch chan struct{} - wg sync.WaitGroup -} - -func New(max int) Semaphore { - return Semaphore{ch: make(chan struct{}, max)} -} - -func (sem *Semaphore) Lock() { - sem.ch <- struct{}{} -} - -func (sem *Semaphore) Unlock() { - <-sem.ch -} - -func (sem *Semaphore) Go(fn func()) { - sem.Lock() - sem.wg.Add(1) - - go func() { - defer sem.Unlock() - defer sem.wg.Done() - - fn() - }() -} - -func (sem *Semaphore) Wait() { - sem.wg.Wait() -} diff --git a/pkg/signature/signature.go b/pkg/signature/signature.go deleted file mode 100644 index 4264dfc0..00000000 --- a/pkg/signature/signature.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -// Package signature implements functions to verify files by their detached signatures. -package signature - -import ( - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/pkg/errors" -) - -// Verify verifies the given file by its signature using the given armored public key. -func Verify(fileBytes, sigBytes []byte, pubKey string) error { - key, err := crypto.NewKeyFromArmored(pubKey) - if err != nil { - return errors.Wrap(err, "failed to load key") - } - - kr, err := crypto.NewKeyRing(key) - if err != nil { - return errors.Wrap(err, "failed to create keyring") - } - - return kr.VerifyDetached( - crypto.NewPlainMessage(fileBytes), - crypto.NewPGPSignature(sigBytes), - crypto.GetUnixTime(), - ) -} diff --git a/pkg/sum/sum_test.go b/pkg/sum/sum_test.go index e8601970..321867f1 100644 --- a/pkg/sum/sum_test.go +++ b/pkg/sum/sum_test.go @@ -27,10 +27,9 @@ import ( ) func TestRecursiveSum(t *testing.T) { - tempDir, err := os.MkdirTemp("", "verify-test") - require.NoError(t, err) + dir := t.TempDir() - createFiles(t, tempDir, + createFiles(t, dir, filepath.Join("a", "1"), filepath.Join("a", "2"), filepath.Join("b", "3"), @@ -39,33 +38,33 @@ func TestRecursiveSum(t *testing.T) { filepath.Join("b", "c", "6"), ) - sumOriginal := sum(t, tempDir) + sumOriginal := sum(t, dir) // Renaming files should produce a different checksum. - require.NoError(t, os.Rename(filepath.Join(tempDir, "a", "1"), filepath.Join(tempDir, "a", "11"))) - sumRenamed := sum(t, tempDir) + require.NoError(t, os.Rename(filepath.Join(dir, "a", "1"), filepath.Join(dir, "a", "11"))) + sumRenamed := sum(t, dir) require.NotEqual(t, sumOriginal, sumRenamed) // Reverting to the original name should produce the same checksum again. - require.NoError(t, os.Rename(filepath.Join(tempDir, "a", "11"), filepath.Join(tempDir, "a", "1"))) - require.Equal(t, sumOriginal, sum(t, tempDir)) + require.NoError(t, os.Rename(filepath.Join(dir, "a", "11"), filepath.Join(dir, "a", "1"))) + require.Equal(t, sumOriginal, sum(t, dir)) // Moving files should produce a different checksum. - require.NoError(t, os.Rename(filepath.Join(tempDir, "a", "1"), filepath.Join(tempDir, "1"))) - sumMoved := sum(t, tempDir) + require.NoError(t, os.Rename(filepath.Join(dir, "a", "1"), filepath.Join(dir, "1"))) + sumMoved := sum(t, dir) require.NotEqual(t, sumOriginal, sumMoved) // Moving files back to their original location should produce the same checksum again. - require.NoError(t, os.Rename(filepath.Join(tempDir, "1"), filepath.Join(tempDir, "a", "1"))) - require.Equal(t, sumOriginal, sum(t, tempDir)) + require.NoError(t, os.Rename(filepath.Join(dir, "1"), filepath.Join(dir, "a", "1"))) + require.Equal(t, sumOriginal, sum(t, dir)) // Changing file data should produce a different checksum. - originalData := modifyFile(t, filepath.Join(tempDir, "a", "1"), []byte("something")) - require.NotEqual(t, sumOriginal, sum(t, tempDir)) + originalData := modifyFile(t, filepath.Join(dir, "a", "1"), []byte("something")) + require.NotEqual(t, sumOriginal, sum(t, dir)) // Reverting file data should produce the original checksum. - modifyFile(t, filepath.Join(tempDir, "a", "1"), originalData) - require.Equal(t, sumOriginal, sum(t, tempDir)) + modifyFile(t, filepath.Join(dir, "a", "1"), originalData) + require.Equal(t, sumOriginal, sum(t, dir)) } func createFiles(t *testing.T, root string, paths ...string) { diff --git a/test/Makefile b/test/Makefile deleted file mode 100644 index 044af462..00000000 --- a/test/Makefile +++ /dev/null @@ -1,52 +0,0 @@ -.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench - -export GO111MODULE=on -export BRIDGE_VERSION:=2.4.8+integrationtests -export VERBOSITY?=fatal -export TEST_DATA=testdata - -# Tests do not run in parallel. This will overrule user settings. -MAKEFLAGS=-j1 - -check-go: - @which go || (echo "Install Go-lang!" && exit 1) -check-godog: - @which godog || $(MAKE) install-godog -install-godog: check-go - go install github.com/cucumber/godog/cmd/godog@v0.12.5 - go install github.com/cucumber/godog/cmd/godog@upd-go1.18 - -test: test-bridge -test-bridge: FEATURES ?= features -test-bridge: check-godog - TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json godog --tags="~@ignore" $(FEATURES) - -# Doesn't work in parallel! -# Provide TEST_ACCOUNTS with your accounts. -test-live: test-live-bridge test-live-ie -test-live-bridge: FEATURES ?= features -test-live-bridge: check-godog - TEST_ENV=live godog --tags="~@ignore && ~@ignore-live && ~@ignore-live-auth" $(FEATURES) - -test-live-bridge-auth: check-godog - TEST_ENV=live godog --tags="@ignore-live-auth" $(FEATURES) - - -# Doesn't work in parallel! -# Provide TEST_ACCOUNTS with your accounts. -# We need to pass build tag which is not possible with godog command. -# Tests against staging env are intended for debug purposes or checking new changes on API. -test-stage: - TEST_ENV=live go test -tags=$(TAGS) -- $(FEATURES) - -test-debug: - TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json dlv test -- $(FEATURES) - -test-live-debug: - TEST_ENV=live dlv test -- $(FEATURES) - -# -run flag is not working anyway, but lets keep it there to note we really do not want to run tests. -# To properly benchmark sync/fetch, we need everything empty. For that is better to start everything -# again and safest way is to run only one loop per run. -bench: - TEST_DATA=../testdata go test -run='^$$' -bench=. -benchtime=1x -timeout=60m ./benchmarks/... diff --git a/test/README.md b/test/README.md deleted file mode 100644 index 08de3544..00000000 --- a/test/README.md +++ /dev/null @@ -1,142 +0,0 @@ -# Integration tests - -This folder contains integration tests of the Bridge app. - -## What and how we are testing - -```mermaid -graph LR - S[Server] - C[Client] - U[User] - Creds[Credentials store] - - subgraph "Bridge app" - Core[Bridge core] - Store - Frontend["Qt / CLI"] - IMAP - SMTP - API[PMAPI] - - IMAP --> Core - SMTP --> Core - Frontend --> Core - Store --> Core - Core --> API - end - - C --> IMAP - C --> SMTP - U --> Frontend - API --> S - Core --> Creds -``` - -We want to test Bridge app from outside as much as possible. So we mock server (API), -credentials store and call commands to IMAP or SMTP the same way as client would do. - -## Running tests - -In order to run Integration tests just go into the test folder `cd test` -and run `make test`. - -You can also test only specific feature (or subset of features) by using `FEATURES` environment -variable: `FEATURES=features/imap/message/create.feature make test`. - -## Example test - -BDD test in gherkin (cucumber) format (https://cucumber.io/docs/gherkin/reference/). - -``` -Feature: IMAP update messages - Background: - Given there is connected user "user" - And there are messages in mailbox "INBOX" for "user" - | from | to | subject | body | read | starred | - | john.doe@mail.com | user@pm.me | foo | hello | false | false | - | jane.doe@mail.com | name@pm.me | bar | world | true | true | - And there is IMAP client logged in as "user" - And there is IMAP client selected in "INBOX" - - Scenario: Mark message as read - When IMAP client marks message "1" as read - Then IMAP response is "OK" - And message "1" in "INBOX" for "user" is marked as read - And message "1" in "INBOX" for "user" is marked as unstarred -``` - -Is translated into code with godog (https://github.com/cucumber/godog/). - -```go -// Registration -func FeatureContext(s *godog.Suite) { - s.Step(`^there is connected user "([^"]*)"$`, thereIsConnectedUser) -} - -// Godog step function -func thereIsConnectedUser(username string) error { - account := ctx.GetTestAccount(username) - if account == nil { - return godog.ErrPending - } - ctx.GetPMAPIController().AddUser(account.User, account.Addresses) - return ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()) -} -``` - -## BDD - -BDD has three parts: - -* `Given` (setup), -* `When` (action) -* and `Then` (check). - -Setup has to prepare context and always end without error. Action, on -the other hand, needs to always end without error, but store it in -the context. Check should analyze the status of the bridge, store or -API and also check whether something failed before. - -Therefore we cannot use a sentence such as `there is user` for both -setup and check steps. We always begin setup steps with `there is/are`, -while check steps are written in the form `something is/has feature`. -Actions are written in the form `something does action`. By doing this -we can always be sure what each steps does or should do. - -In the code, we separate those parts in its own files to make sure -it's clear how the function should be implemented. - -In the `Given` phase is also generally better to setup data (as `there are messages...`) -first, then users (`there is connected user...`) and then connections (`there is IMAP client...`). -This can prevent some hitches in internal implementation of integration tests. - -## API faked by fakeapi or liveapi - -We need to control what server returns. Instead of using raw JSONs, -we fake the whole pmapi for local testing. Fake pmapi behaves as much -as possible the same way as real server, but does not follow every -single detail. Otherwise we would end up with writing complete server. :-) - -For both -- fake local pmapi and real live server -- we use controller. -Controller is available on test context and does setup like setting up -internet connection, user settings, labels or messages. - -Accounts for each environment are set up in `accounts` folder. Each -test function should use `TestAccount` object obtained by test ID -(such as `user` or `userMultipleAddress` for users, or `primary` -or `secondary` for addresses) and use available functions to get real -IDs (even if fake API uses the test IDs as real ones). - -Testing against live is using real users and doesn't work in parallel. -Only one job against live at a time can be running. - -## External e-mail accounts - -We have some external accounts which we are using for testing: - -* pm.bridge.qa@gmail.com -* bridge-qa@yandex.ru -* bridgeqa@seznam.cz - -For access, ask bridge team. diff --git a/test/accounts/account.go b/test/accounts/account.go deleted file mode 100644 index 6aee9e82..00000000 --- a/test/accounts/account.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge.Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package accounts - -import ( - "encoding/json" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" -) - -const ( - testUserKey = "user_key.json" - testAddressKey = "address_key.json" -) - -type TestAccount struct { - user *pmapi.User - addressToBeUsed *pmapi.Address - addressesByBDDAddressID map[string]*pmapi.Address - password string - mailboxPassword string - twoFAEnabled bool -} - -func newTestAccount( - user *pmapi.User, - addressesByBDDAddressID map[string]*pmapi.Address, - addressIDToBeUsed string, - password, - mailboxPassword string, - twoFAEnabled bool, -) *TestAccount { - account := &TestAccount{ - user: user, - addressesByBDDAddressID: addressesByBDDAddressID, - password: password, - mailboxPassword: mailboxPassword, - twoFAEnabled: twoFAEnabled, - } - - if addressIDToBeUsed == "" { - account.addressToBeUsed = account.Addresses().Main() - } else { - for addressID, address := range addressesByBDDAddressID { - if addressID == addressIDToBeUsed { - account.addressToBeUsed = address - } - } - } - if account.addressToBeUsed == nil { - // Return nothing which will be interpreted as not implemented the same way the whole account. - return nil - } - - account.initKeys() - return account -} - -func (a *TestAccount) initKeys() { - userKeys := loadPMKeys(readTestFile(testUserKey)) - - addressKeys := loadPMKeys(readTestFile(testAddressKey)) - - a.user.Keys = *userKeys - for _, addressEmail := range a.Addresses().ActiveEmails() { - a.Addresses().ByEmail(addressEmail).Keys = *addressKeys - } -} - -func readTestFile(fileName string) []byte { - testDataFolder := os.Getenv("TEST_DATA") - path := filepath.Join(testDataFolder, fileName) - data, err := os.ReadFile(path) //nolint:gosec - if err != nil { - panic(err) - } - return data -} - -func loadPMKeys(jsonKeys []byte) (keys *pmapi.PMKeys) { - _ = json.Unmarshal(jsonKeys, &keys) - return -} - -func (a *TestAccount) User() *pmapi.User { - return a.user -} - -func (a *TestAccount) UserID() string { - return a.user.ID -} - -func (a *TestAccount) Username() string { - return a.user.Name -} - -func (a *TestAccount) Addresses() *pmapi.AddressList { - addressArray := []*pmapi.Address{} - for _, address := range a.addressesByBDDAddressID { - addressArray = append(addressArray, address) - } - // The order of addresses is important in PMAPI because the primary - // address is always the first in array. We are using map to define - // testing addresses which can cause random re-schuffle between tests - sort.SliceStable( - addressArray, - func(i, j int) bool { - return addressArray[i].Order < addressArray[j].Order - }, - ) - addresses := pmapi.AddressList(addressArray) - return &addresses -} - -func (a *TestAccount) Address() string { - return a.addressToBeUsed.Email -} - -func (a *TestAccount) AddressID() string { - return a.addressToBeUsed.ID -} - -func (a *TestAccount) GetAddressID(addressTestID string) string { - return a.addressesByBDDAddressID[addressTestID].ID -} - -// EnsureAddressID accepts address (simply the address) or bddAddressID used -// in tests (in format [bddAddressID]) and returns always the real address ID. -// If the address is not found, the ID of main address is returned. -func (a *TestAccount) EnsureAddressID(addressOrAddressTestID string) string { - if strings.HasPrefix(addressOrAddressTestID, "[") { - addressTestID := addressOrAddressTestID[1 : len(addressOrAddressTestID)-1] - address := a.addressesByBDDAddressID[addressTestID] - return address.ID - } - for _, address := range a.addressesByBDDAddressID { - if address.Email == addressOrAddressTestID { - return address.ID - } - } - return a.AddressID() -} - -func (a *TestAccount) GetAddress(addressTestID string) string { - return a.addressesByBDDAddressID[addressTestID].Email -} - -// EnsureAddress accepts address (simply the address) or bddAddressID used -// in tests (in format [bddAddressID]) and returns always the address. -// If the address ID cannot be found, the original value is returned. -func (a *TestAccount) EnsureAddress(addressOrAddressTestID string) string { - if strings.HasPrefix(addressOrAddressTestID, "[") { - addressTestID := addressOrAddressTestID[1 : len(addressOrAddressTestID)-1] - address := a.addressesByBDDAddressID[addressTestID] - return address.Email - } - return addressOrAddressTestID -} - -func (a *TestAccount) Password() []byte { - return []byte(a.password) -} - -func (a *TestAccount) MailboxPassword() []byte { - return []byte(a.mailboxPassword) -} - -func (a *TestAccount) IsTwoFAEnabled() bool { - return a.twoFAEnabled -} - -func (a *TestAccount) BridgePassword() string { - return BridgePassword -} diff --git a/test/accounts/accounts.go b/test/accounts/accounts.go deleted file mode 100644 index 143b43c2..00000000 --- a/test/accounts/accounts.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge.Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package accounts - -import ( - "encoding/json" - "os" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/pkg/errors" -) - -// BridgePassword is password to be used for IMAP or SMTP under tests. -const BridgePassword = "bridgepassword" - -type TestAccounts struct { - Users map[string]*pmapi.User // Key is user ID used in BDD. - Addresses map[string]map[string]*pmapi.Address // Key is real user ID, second key is address ID used in BDD. - Passwords map[string]string // Key is real user ID. - MailboxPasswords map[string]string // Key is real user ID. - TwoFAs map[string]bool // Key is real user ID. -} - -func Load(path string) (*TestAccounts, error) { - data, err := os.ReadFile(path) //nolint:gosec - if err != nil { - return nil, errors.Wrap(err, "failed to load JSON") - } - - var testAccounts TestAccounts - err = json.Unmarshal(data, &testAccounts) - if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal JSON") - } - - return &testAccounts, nil -} - -func (a *TestAccounts) GetTestAccount(username string) *TestAccount { - return a.GetTestAccountWithAddress(username, "") -} - -// GetTestAccountWithAddress returns the test account with the given username configured to use the given bddAddressID. -func (a *TestAccounts) GetTestAccountWithAddress(username, bddAddressID string) *TestAccount { - // Do lookup by full address and convert to name in tests. - // Used by getting real data to ensure correct address or address ID. - for key, user := range a.Users { - if user.Name == username { - username = key - break - } - } - user, ok := a.Users[username] - if !ok { - return nil - } - return newTestAccount( - user, - a.Addresses[user.Name], - bddAddressID, - a.Passwords[user.Name], - a.MailboxPasswords[user.Name], - a.TwoFAs[user.Name], - ) -} diff --git a/test/accounts/fake.json b/test/accounts/fake.json deleted file mode 100644 index e1859af3..00000000 --- a/test/accounts/fake.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "users": { - "user": { - "ID": "1", - "Name": "user", - "MaxUpload": 26214400, - "UsedSpace": 1048576, - "MaxSpace": 10485760 - }, - "user2fa": { - "ID": "2", - "Name": "user2fa", - "MaxUpload": 26214400, - "UsedSpace": 1048576, - "MaxSpace": 10485760 - }, - "userAddressWithCapitalLetter": { - "ID": "3", - "Name": "userAddressWithCapitalLetter", - "MaxUpload": 26214400, - "UsedSpace": 1048576, - "MaxSpace": 10485760 - }, - "userMoreAddresses": { - "ID": "4", - "Name": "userMoreAddresses", - "MaxUpload": 26214400, - "UsedSpace": 1048576, - "MaxSpace": 10485760 - }, - "userDisabledPrimaryAddress": { - "ID": "5", - "Name": "userDisabledPrimaryAddress", - "MaxUpload": 26214400, - "UsedSpace": 1048576, - "MaxSpace": 10485760 - } - }, - "addresses": { - "user": { - "userAddress": { - "ID": "userAddress", - "Email": "user@pm.me", - "Order": 1, - "Receive": 1, - "HasKeys": 1 - } - }, - "user2fa": { - "user2faAddress": { - "ID": "user2faAddress", - "Email": "user@pm.me", - "Order": 1, - "Receive": 1, - "HasKeys": 1 - } - }, - "userAddressWithCapitalLetter": { - "userAddressWithCapitalLetterAddress": { - "ID": "userAddressWithCapitalLetterAddress", - "Email": "uSeR@pm.me", - "Order": 1, - "Receive": 1, - "HasKeys": 1 - } - }, - "userMoreAddresses": { - "primary": { - "ID": "primary", - "Email": "primaryaddress@pm.me", - "Order": 1, - "Receive": 1, - "HasKeys": 1 - }, - "secondary": { - "ID": "secondary", - "Email": "secondaryaddress@pm.me", - "Order": 2, - "Receive": 1, - "HasKeys": 1 - }, - "disabled": { - "ID": "disabled", - "Email": "disabledaddress@pm.me", - "Order": 3, - "Receive": 0 - } - }, - "userDisabledPrimaryAddress": { - "primary": { - "ID": "primary", - "Email": "user@pm.me", - "Order": 1, - "Receive": 0 - }, - "secondary": { - "ID": "secondary", - "Email": "secondaryaddress@pm.me", - "Order": 2, - "Receive": 1, - "HasKeys": 1 - } - } - }, - "passwords": { - "user": "password", - "user2fa": "password", - "userAddressWithCapitalLetter": "password", - "userMoreAddresses": "password", - "userDisabledPrimaryAddress": "password" - }, - "mailboxPasswords": { - "user": "testpassphrase", - "user2fa": "testpassphrase", - "userAddressWithCapitalLetter": "testpassphrase", - "userMoreAddresses": "testpassphrase", - "userDisabledPrimaryAddress": "testpassphrase" - }, - "twoFAs": { - "user": false, - "user2fa": true, - "userAddressWithCapitalLetter": false, - "userMoreAddresses": false, - "userDisabledPrimaryAddress": false - } -} diff --git a/test/api_actions_test.go b/test/api_actions_test.go deleted file mode 100644 index e443d6ce..00000000 --- a/test/api_actions_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge.Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package tests - -import ( - "time" - - "github.com/cucumber/godog" -) - -func APIActionsFeatureContext(s *godog.ScenarioContext) { - s.Step(`^the internet connection is lost$`, theInternetConnectionIsLost) - s.Step(`^the internet connection is restored$`, theInternetConnectionIsRestored) - s.Step(`^(\d+) second[s]? pass$`, secondsPass) - s.Step(`^the body of draft "([^"]*)" for "([^"]*)" has changed to "([^"]*)"$`, draftBodyChanged) -} - -func theInternetConnectionIsLost() error { - ctx.GetPMAPIController().TurnInternetConnectionOff() - return nil -} - -func theInternetConnectionIsRestored() error { - ctx.GetPMAPIController().TurnInternetConnectionOn() - return nil -} - -func secondsPass(seconds int) error { - time.Sleep(time.Duration(seconds) * time.Second) - return nil -} - -func draftBodyChanged(bddMessageID, bddUserID, body string) error { - account := ctx.GetTestAccount(bddUserID) - if account == nil { - return godog.ErrPending - } - - messageID, err := ctx.GetAPIMessageID(account.Username(), bddMessageID) - if err != nil { - return internalError(err, "getting apiID for %s", bddMessageID) - } - - err = ctx.GetPMAPIController().SetDraftBody(account.Username(), messageID, body) - if err != nil { - return internalError(err, "cannot set body of %s", messageID) - } - - return nil -} diff --git a/test/api_checks_test.go b/test/api_checks_test.go deleted file mode 100644 index 574f6694..00000000 --- a/test/api_checks_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge.Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package tests - -import ( - "fmt" - "regexp" - "runtime" - "strings" - "time" - - "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/ProtonMail/proton-bridge/v2/test/accounts" - "github.com/cucumber/godog" - "github.com/stretchr/testify/assert" -) - -func APIChecksFeatureContext(s *godog.ScenarioContext) { - s.Step(`^API endpoint "([^"]*)" is called$`, apiIsCalled) - s.Step(`^API endpoint "([^"]*)" is called with$`, apiIsCalledWith) - s.Step(`^API endpoint "([^"]*)" is not called$`, apiIsNotCalled) - s.Step(`^API endpoint "([^"]*)" is not called with$`, apiIsNotCalledWith) - s.Step(`^message is sent with API call$`, messageIsSentWithAPICall) - s.Step(`^packages are sent with API call$`, packagesAreSentWithAPICall) - s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has (\d+) message(?:s)?$`, apiMailboxForUserHasNumberOfMessages) - s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has (\d+) message(?:s)?$`, apiMailboxForAddressOfUserHasNumberOfMessages) - s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages) - s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages) - s.Step(`^API user-agent is "([^"]*)"$`, userAgent) -} - -func apiIsCalled(endpoint string) error { - if !apiIsCalledWithHelper(endpoint, "") { - return fmt.Errorf("%s was not called", endpoint) - } - return nil -} - -func apiIsCalledWith(endpoint string, data *godog.DocString) error { - if !apiIsCalledWithHelper(endpoint, data.Content) { - return fmt.Errorf("%s was not called with %s", endpoint, data.Content) - } - return nil -} - -func apiIsCalledWithRegex(endpoint string, data *godog.DocString) error { - match, err := apiIsCalledWithHelperRegex(endpoint, data.Content) - if err != nil { - return err - } - if !match { - return fmt.Errorf("%s was not called with %s", endpoint, data.Content) - } - return nil -} - -func apiIsNotCalled(endpoint string) error { - if apiIsCalledWithHelper(endpoint, "") { - return fmt.Errorf("%s was called", endpoint) - } - return nil -} - -func apiIsNotCalledWith(endpoint string, data *godog.DocString) error { - if apiIsCalledWithHelper(endpoint, data.Content) { - return fmt.Errorf("%s was called with %s", endpoint, data.Content) - } - return nil -} - -func apiIsCalledWithHelper(endpoint string, content string) bool { - split := strings.Split(endpoint, " ") - method := split[0] - path := split[1] - request := []byte(content) - return ctx.GetPMAPIController().WasCalled(method, path, request) -} - -func apiIsCalledWithHelperRegex(endpoint string, content string) (bool, error) { - split := strings.Split(endpoint, " ") - method := split[0] - path := split[1] - request := []byte(content) - return ctx.GetPMAPIController().WasCalledRegex(method, path, request) -} - -func messageIsSentWithAPICall(data *godog.DocString) error { - endpoint := "POST /mail/v4/messages" - if err := apiIsCalledWith(endpoint, data); err != nil { - return err - } - for _, request := range ctx.GetPMAPIController().GetCalls("POST", "/mail/v4/messages") { - if !checkAllRequiredFieldsForSendingMessage(request) { - return fmt.Errorf("%s was not called with all required fields: %s", endpoint, request) - } - } - - return nil -} - -func packagesAreSentWithAPICall(data *godog.DocString) error { - endpoint := "POST /mail/v4/messages/.+$" - if err := apiIsCalledWithRegex(endpoint, data); err != nil { - return err - } - for _, request := range ctx.GetPMAPIController().GetCalls("POST", "/mail/v4/messages") { - if !checkAllRequiredFieldsForSendingMessage(request) { - return fmt.Errorf("%s was not called with all required fields: %s", endpoint, request) - } - } - - return nil -} - -func checkAllRequiredFieldsForSendingMessage(request []byte) bool { - if matches := regexp.MustCompile(`"Subject":`).Match(request); !matches { - return false - } - if matches := regexp.MustCompile(`"ToList":`).Match(request); !matches { - return false - } - if matches := regexp.MustCompile(`"CCList":`).Match(request); !matches { - return false - } - if matches := regexp.MustCompile(`"BCCList":`).Match(request); !matches { - return false - } - if matches := regexp.MustCompile(`"AddressID":`).Match(request); !matches { - return false - } - if matches := regexp.MustCompile(`"Body":`).Match(request); !matches { - return false - } - return true -} - -func apiMailboxForUserHasNumberOfMessages(mailboxName, bddUserID string, countOfMessages int) error { - return apiMailboxForAddressOfUserHasNumberOfMessages(mailboxName, "", bddUserID, countOfMessages) -} - -func apiMailboxForAddressOfUserHasNumberOfMessages(mailboxName, bddAddressID, bddUserID string, countOfMessages int) error { - account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID) - if account == nil { - return godog.ErrPending - } - - start := time.Now() - for { - afterLimit := time.Since(start) > ctx.EventLoopTimeout() - pmapiMessages, err := getPMAPIMessages(account, mailboxName) - if err != nil { - return err - } - total := len(pmapiMessages) - if total == countOfMessages { - break - } - if afterLimit { - return fmt.Errorf("expected %v messages, but got %v", countOfMessages, total) - } - time.Sleep(100 * time.Millisecond) - } - return nil -} - -func apiMailboxForUserHasMessages(mailboxName, bddUserID string, messages *godog.Table) error { - return apiMailboxForAddressOfUserHasMessages(mailboxName, "", bddUserID, messages) -} - -func apiMailboxForAddressOfUserHasMessages(mailboxName, bddAddressID, bddUserID string, messages *godog.Table) error { - account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID) - if account == nil { - return godog.ErrPending - } - - pmapiMessages, err := getPMAPIMessages(account, mailboxName) - if err != nil { - return err - } - - head := messages.Rows[0].Cells - for _, row := range messages.Rows[1:] { - found, err := pmapiMessagesContainsMessageRow(account, pmapiMessages, head, row) - if err != nil { - return err - } - if !found { - rowMap := map[string]string{} - for idx, cell := range row.Cells { - rowMap[head[idx].Value] = cell.Value - } - return fmt.Errorf("message %v not found", rowMap) - } - } - return nil -} - -func getPMAPIMessages(account *accounts.TestAccount, mailboxName string) ([]*pmapi.Message, error) { - labelIDs, err := ctx.GetPMAPIController().GetLabelIDs(account.Username(), []string{mailboxName}) - if err != nil { - return nil, internalError(err, "getting label %s for %s", mailboxName, account.Username()) - } - labelID := labelIDs[0] - - return ctx.GetPMAPIController().GetMessages(account.Username(), labelID) -} - -func userAgent(expectedUserAgent string) error { - expectedUserAgent = strings.ReplaceAll(expectedUserAgent, "[GOOS]", runtime.GOOS) - - assert.Eventually(ctx.GetTestingT(), func() bool { - return ctx.GetUserAgent() == expectedUserAgent - }, 5*time.Second, time.Second) - - return nil -} diff --git a/test/api_setup_test.go b/test/api_setup_test.go deleted file mode 100644 index caa65f08..00000000 --- a/test/api_setup_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge.Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package tests - -import ( - "github.com/cucumber/godog" -) - -func APISetupFeatureContext(s *godog.ScenarioContext) { - s.Step(`^there is no internet connection$`, thereIsNoInternetConnection) -} - -func thereIsNoInternetConnection() error { - ctx.GetPMAPIController().TurnInternetConnectionOff() - return nil -} diff --git a/test/bdd_test.go b/test/bdd_test.go deleted file mode 100644 index 6bfd368b..00000000 --- a/test/bdd_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2022 Proton AG -// -// This file is part of Proton Mail Bridge.Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package tests - -import ( - "context" - - testContext "github.com/ProtonMail/proton-bridge/v2/test/context" - "github.com/cucumber/godog" -) - -const ( - timeFormat = "2006-01-02T15:04:05" -) - -func SuiteInitializer(s *godog.TestSuiteContext) { - s.BeforeSuite(testContext.BeforeRun) - s.AfterSuite(testContext.AfterRun) -} - -func ScenarioInitializer(s *godog.ScenarioContext) { - s.Before(beforeScenario) - s.After(afterScenario) - - APIActionsFeatureContext(s) - APIChecksFeatureContext(s) - APISetupFeatureContext(s) - - BridgeActionsFeatureContext(s) - - CommonChecksFeatureContext(s) - - IMAPActionsAuthFeatureContext(s) - IMAPActionsMailboxFeatureContext(s) - IMAPActionsMessagesFeatureContext(s) - IMAPChecksFeatureContext(s) - IMAPSetupFeatureContext(s) - - SMTPActionsAuthFeatureContext(s) - SMTPChecksFeatureContext(s) - SMTPSetupFeatureContext(s) - - StoreActionsFeatureContext(s) - StoreChecksFeatureContext(s) - StoreSetupFeatureContext(s) - - UsersActionsFeatureContext(s) - UsersSetupFeatureContext(s) - UsersChecksFeatureContext(s) -} - -var ctx *testContext.TestContext //nolint:gochecknoglobals - -func beforeScenario(scenarioCtx context.Context, _ *godog.Scenario) (context.Context, error) { - ctx = testContext.New() - return scenarioCtx, nil -} - -func afterScenario(scenarioCtx context.Context, _ *godog.Scenario, err error) (context.Context, error) { - if err != nil { - for _, user := range ctx.GetUsers().GetUsers() { - store := user.GetStore() - if store != nil { - store.TestDumpDB(ctx.GetTestingT()) - } - } - } - ctx.Cleanup() - if err != nil { - ctx.GetPMAPIController().PrintCalls() - } - - return scenarioCtx, err -} diff --git a/test/benchmarks/bench_results/human-table.py b/test/benchmarks/bench_results/human-table.py deleted file mode 100644 index 5ccd0788..00000000 --- a/test/benchmarks/bench_results/human-table.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2022 Proton AG -# -# This file is part of Proton Mail Bridge. -# -# Proton Mail Bridge is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Proton Mail Bridge is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Proton Mail Bridge. If not, see . - - -import glob -import pandas as pd -import re - - -def print_header(report_file): - print('\n======== %s ========' % - (report_file.replace("./bench-", "").replace(".log", ""))) - - -rx_line = { - 'exists': re.compile(r'.*Res[A-Za-z]?: [*] (?P\d+) EXISTS.*\n'), - 'bench': re.compile(r'Benchmark(?P[^ \t]+)[ \t]+(?P\d+)[ \t]+(?P\d+) ns/op.*\n'), - # 'total' : re.compile(r'ok[ \t]+(?P[^ \t]+)[ \t]+(?P