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
-