mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-15 14:56:42 +00:00
GODT-980: placeholder for user agent
This commit is contained in:
@ -43,19 +43,20 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/cache"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/tls"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -86,6 +87,7 @@ type Base struct {
|
||||
Creds *credentials.Store
|
||||
CM *pmapi.ClientManager
|
||||
CookieJar *cookies.Jar
|
||||
UserAgent *useragent.UserAgent
|
||||
Updater *updater.Updater
|
||||
Versioner *versioner.Versioner
|
||||
TLS *tls.TLS
|
||||
@ -107,7 +109,10 @@ func New( // nolint[funlen]
|
||||
keychainName,
|
||||
cacheVersion string,
|
||||
) (*Base, error) {
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version)
|
||||
userAgent := useragent.New()
|
||||
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
|
||||
|
||||
crashHandler := crash.NewHandler(
|
||||
sentryReporter.ReportException,
|
||||
crash.ShowErrorNotification(appName),
|
||||
@ -181,20 +186,9 @@ func New( // nolint[funlen]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiConfig := pmapi.GetAPIConfig(configName, constants.Version)
|
||||
apiConfig.ConnectionOffHandler = func() {
|
||||
listener.Emit(events.InternetOffEvent, "")
|
||||
}
|
||||
apiConfig.ConnectionOnHandler = func() {
|
||||
listener.Emit(events.InternetOnEvent, "")
|
||||
}
|
||||
apiConfig.UpgradeApplicationHandler = func() {
|
||||
listener.Emit(events.UpgradeApplicationEvent, "")
|
||||
}
|
||||
cm := pmapi.NewClientManager(apiConfig)
|
||||
cm := pmapi.NewClientManager(getAPIConfig(configName, listener), userAgent)
|
||||
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
|
||||
cm.SetCookieJar(jar)
|
||||
sentryReporter.SetUserAgentProvider(cm)
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
@ -245,6 +239,7 @@ func New( // nolint[funlen]
|
||||
Creds: credentials.NewStore(kc),
|
||||
CM: cm,
|
||||
CookieJar: jar,
|
||||
UserAgent: userAgent,
|
||||
Updater: updater,
|
||||
Versioner: versioner,
|
||||
TLS: tls.New(settingsPath),
|
||||
@ -380,3 +375,13 @@ func (b *Base) doTeardown() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAPIConfig(configName string, listener listener.Listener) *pmapi.ClientConfig {
|
||||
apiConfig := pmapi.GetAPIConfig(configName, constants.Version)
|
||||
|
||||
apiConfig.ConnectionOffHandler = func() { listener.Emit(events.InternetOffEvent, "") }
|
||||
apiConfig.ConnectionOnHandler = func() { listener.Emit(events.InternetOnEvent, "") }
|
||||
apiConfig.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
|
||||
|
||||
return apiConfig
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
b.CrashHandler,
|
||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
|
||||
imapPort, tlsConfig, imapBackend, b.Listener).ListenAndServe()
|
||||
imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
@ -130,6 +130,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
b.Settings,
|
||||
b.Listener,
|
||||
b.Updater,
|
||||
b.UserAgent,
|
||||
bridge,
|
||||
smtpBackend,
|
||||
b.Autostart,
|
||||
|
||||
@ -26,10 +26,10 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
@ -47,10 +47,6 @@ type Bridge struct {
|
||||
clientManager users.ClientManager
|
||||
updater Updater
|
||||
versioner Versioner
|
||||
|
||||
userAgentClientName string
|
||||
userAgentClientVersion string
|
||||
userAgentOS string
|
||||
}
|
||||
|
||||
func New(
|
||||
@ -120,40 +116,6 @@ func (b *Bridge) heartbeat() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
|
||||
func (b *Bridge) GetCurrentClient() string {
|
||||
res := b.userAgentClientName
|
||||
if b.userAgentClientVersion != "" {
|
||||
res = res + " " + b.userAgentClientVersion
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// SetCurrentClient updates client info (e.g. Thunderbird) and sets the user agent
|
||||
// on pmapi. By default no client is used, IMAP has to detect it on first login.
|
||||
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
|
||||
b.userAgentClientName = clientName
|
||||
b.userAgentClientVersion = clientVersion
|
||||
b.updateUserAgent()
|
||||
}
|
||||
|
||||
// SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
|
||||
// `runtime.GOOS`, but this can be overridden in case of better detection.
|
||||
func (b *Bridge) SetCurrentOS(os string) {
|
||||
b.userAgentOS = os
|
||||
b.updateUserAgent()
|
||||
}
|
||||
|
||||
func (b *Bridge) updateUserAgent() {
|
||||
logrus.
|
||||
WithField("clientName", b.userAgentClientName).
|
||||
WithField("clientVersion", b.userAgentClientVersion).
|
||||
WithField("OS", b.userAgentOS).
|
||||
Info("Updating user agent")
|
||||
|
||||
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
|
||||
}
|
||||
|
||||
// ReportBug reports a new bug from the user.
|
||||
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
|
||||
c := b.clientManager.GetAnonymousClient()
|
||||
|
||||
@ -21,10 +21,10 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
)
|
||||
|
||||
type storeFactory struct {
|
||||
|
||||
51
internal/config/useragent/platform.go
Normal file
51
internal/config/useragent/platform.go
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2021 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
// IsCatalinaOrNewer checks whether host is MacOS Catalina 10.15.x or higher.
|
||||
func IsCatalinaOrNewer() bool {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return false
|
||||
}
|
||||
|
||||
rawVersion, err := exec.Command("sw_vers", "-productVersion").Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return isVersionCatalinaOrNewer(strings.TrimSpace(string(rawVersion)))
|
||||
}
|
||||
|
||||
func isVersionCatalinaOrNewer(rawVersion string) bool {
|
||||
semVersion, err := semver.NewVersion(rawVersion)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
minVersion := semver.MustParse("10.15.0")
|
||||
|
||||
return semVersion.GreaterThan(minVersion) || semVersion.Equal(minVersion)
|
||||
}
|
||||
44
internal/config/useragent/platform_test.go
Normal file
44
internal/config/useragent/platform_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2021 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsVersionCatalinaOrNewer(t *testing.T) {
|
||||
testData := map[struct{ version string }]bool{
|
||||
{""}: false,
|
||||
{"9.0.0"}: false,
|
||||
{"9.15.0"}: false,
|
||||
{"10.13.0"}: false,
|
||||
{"10.14.0"}: false,
|
||||
{"10.14.99"}: false,
|
||||
{"10.15.0"}: true,
|
||||
{"10.16.0"}: true,
|
||||
{"11.0.0"}: true,
|
||||
{"11.1"}: true,
|
||||
}
|
||||
|
||||
for args, exp := range testData {
|
||||
got := isVersionCatalinaOrNewer(args.version)
|
||||
assert.Equal(t, exp, got, "version %v", args.version)
|
||||
}
|
||||
}
|
||||
59
internal/config/useragent/useragent.go
Normal file
59
internal/config/useragent/useragent.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2021 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type UserAgent struct {
|
||||
client, platform string
|
||||
}
|
||||
|
||||
func New() *UserAgent {
|
||||
return &UserAgent{
|
||||
client: "",
|
||||
platform: runtime.GOOS,
|
||||
}
|
||||
}
|
||||
|
||||
func (ua *UserAgent) SetClient(name, version string) {
|
||||
ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2"))
|
||||
}
|
||||
|
||||
func (ua *UserAgent) HasClient() bool {
|
||||
return ua.client != ""
|
||||
}
|
||||
|
||||
func (ua *UserAgent) SetPlatform(platform string) {
|
||||
ua.platform = platform
|
||||
}
|
||||
|
||||
func (ua *UserAgent) String() string {
|
||||
var client string
|
||||
|
||||
if ua.client != "" {
|
||||
client = ua.client
|
||||
} else {
|
||||
client = "NoClient/0.0.1"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v (%v)", client, ua.platform)
|
||||
}
|
||||
86
internal/config/useragent/useragent_test.go
Normal file
86
internal/config/useragent/useragent_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2021 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserAgent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, version, platform string
|
||||
want string
|
||||
}{
|
||||
// No name/version, no platform.
|
||||
{
|
||||
want: fmt.Sprintf("NoClient/0.0.1 (%v)", runtime.GOOS),
|
||||
},
|
||||
|
||||
// No name/version, with platform.
|
||||
{
|
||||
platform: "macOS 10.15",
|
||||
want: "NoClient/0.0.1 (macOS 10.15)",
|
||||
},
|
||||
|
||||
// With name/version, with platform.
|
||||
{
|
||||
name: "Mac OS X Mail",
|
||||
version: "1.0.0",
|
||||
platform: "macOS 10.15",
|
||||
want: "Mac OS X Mail/1.0.0 (macOS 10.15)",
|
||||
},
|
||||
|
||||
// With name/version, with platform.
|
||||
{
|
||||
name: "Mac OS X Mail",
|
||||
version: "13.4 (3608.120.23.2.4)",
|
||||
platform: "macOS 10.15",
|
||||
want: "Mac OS X Mail/13.4-3608.120.23.2.4 (macOS 10.15)",
|
||||
},
|
||||
|
||||
// With name/version, with platform.
|
||||
{
|
||||
name: "Thunderbird",
|
||||
version: "78.6.1",
|
||||
platform: "Windows 10 (10.0)",
|
||||
want: "Thunderbird/78.6.1 (Windows 10 (10.0))",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(test.want, func(t *testing.T) {
|
||||
ua := New()
|
||||
|
||||
if test.name != "" && test.version != "" {
|
||||
ua.SetClient(test.name, test.version)
|
||||
}
|
||||
|
||||
if test.platform != "" {
|
||||
ua.SetPlatform(test.platform)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.want, ua.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -19,7 +19,7 @@
|
||||
package crash
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
|
||||
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
|
||||
@ -60,6 +61,7 @@ func New(
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
userAgent *useragent.UserAgent,
|
||||
bridge *bridge.Bridge,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
@ -77,6 +79,7 @@ func New(
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
userAgent,
|
||||
bridgeWrap,
|
||||
noEncConfirmator,
|
||||
autostart,
|
||||
@ -95,6 +98,7 @@ func newBridgeFrontend(
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
userAgent *useragent.UserAgent,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
@ -122,6 +126,7 @@ func newBridgeFrontend(
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
userAgent,
|
||||
bridge,
|
||||
noEncConfirmator,
|
||||
autostart,
|
||||
|
||||
@ -39,6 +39,7 @@ import (
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
@ -49,7 +50,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/useragent"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/therecipe/qt/core"
|
||||
@ -75,6 +75,7 @@ type FrontendQt struct {
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
updater types.Updater
|
||||
userAgent *useragent.UserAgent
|
||||
bridge types.Bridger
|
||||
noEncConfirmator types.NoEncConfirmator
|
||||
|
||||
@ -114,12 +115,15 @@ func New(
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
userAgent *useragent.UserAgent,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
restarter types.Restarter,
|
||||
) *FrontendQt {
|
||||
tmp := &FrontendQt{
|
||||
userAgent.SetPlatform(core.QSysInfo_PrettyProductName())
|
||||
|
||||
f := &FrontendQt{
|
||||
version: version,
|
||||
buildVersion: buildVersion,
|
||||
programName: programName,
|
||||
@ -129,6 +133,7 @@ func New(
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
updater: updater,
|
||||
userAgent: userAgent,
|
||||
bridge: bridge,
|
||||
noEncConfirmator: noEncConfirmator,
|
||||
programVer: "v" + version,
|
||||
@ -138,13 +143,9 @@ func New(
|
||||
|
||||
// Initializing.Done is only called sync.Once. Please keep the increment
|
||||
// set to 1
|
||||
tmp.initializing.Add(1)
|
||||
f.initializing.Add(1)
|
||||
|
||||
// Nicer string for OS.
|
||||
currentOS := core.QSysInfo_PrettyProductName()
|
||||
bridge.SetCurrentOS(currentOS)
|
||||
|
||||
return tmp
|
||||
return f
|
||||
}
|
||||
|
||||
// InstanceExistAlert is a global warning window indicating an instance already exists.
|
||||
@ -506,7 +507,7 @@ func (s *FrontendQt) sendBug(description, client, address string) (isOK bool) {
|
||||
}
|
||||
|
||||
func (s *FrontendQt) getLastMailClient() string {
|
||||
return s.bridge.GetCurrentClient()
|
||||
return s.userAgent.String()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
@ -71,6 +72,7 @@ func New(
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
userAgent *useragent.UserAgent,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
|
||||
@ -75,8 +75,6 @@ type User interface {
|
||||
type Bridger interface {
|
||||
UserManager
|
||||
|
||||
GetCurrentClient() string
|
||||
SetCurrentOS(os string)
|
||||
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
|
||||
AllowProxy()
|
||||
DisallowProxy()
|
||||
|
||||
@ -29,7 +29,6 @@ type cacheProvider interface {
|
||||
}
|
||||
|
||||
type bridger interface {
|
||||
SetCurrentClient(clientName, clientVersion string)
|
||||
GetUser(query string) (bridgeUser, error)
|
||||
}
|
||||
|
||||
|
||||
@ -23,13 +23,13 @@ import (
|
||||
)
|
||||
|
||||
type currentClientSetter interface {
|
||||
SetCurrentClient(name, version string)
|
||||
SetClient(name, version string)
|
||||
}
|
||||
|
||||
// Extension for IMAP server
|
||||
type extension struct {
|
||||
extID imapserver.ConnExtension
|
||||
setter currentClientSetter
|
||||
extID imapserver.ConnExtension
|
||||
clientSetter currentClientSetter
|
||||
}
|
||||
|
||||
func (ext *extension) Capabilities(conn imapserver.Conn) []string {
|
||||
@ -44,8 +44,8 @@ func (ext *extension) Command(name string) imapserver.HandlerFactory {
|
||||
return func() imapserver.Handler {
|
||||
if hdlrID, ok := newIDHandler().(*imapid.Handler); ok {
|
||||
return &handler{
|
||||
hdlrID: hdlrID,
|
||||
setter: ext.setter,
|
||||
hdlrID: hdlrID,
|
||||
clientSetter: ext.clientSetter,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -57,8 +57,8 @@ func (ext *extension) NewConn(conn imapserver.Conn) imapserver.Conn {
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
hdlrID *imapid.Handler
|
||||
setter currentClientSetter
|
||||
hdlrID *imapid.Handler
|
||||
clientSetter currentClientSetter
|
||||
}
|
||||
|
||||
func (hdlr *handler) Parse(fields []interface{}) error {
|
||||
@ -69,21 +69,18 @@ func (hdlr *handler) Handle(conn imapserver.Conn) error {
|
||||
err := hdlr.hdlrID.Handle(conn)
|
||||
if err == nil {
|
||||
id := hdlr.hdlrID.Command.ID
|
||||
hdlr.setter.SetCurrentClient(
|
||||
id[imapid.FieldName],
|
||||
id[imapid.FieldVersion],
|
||||
)
|
||||
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, setter currentClientSetter) imapserver.Extension {
|
||||
func NewExtension(serverID imapid.ID, clientSetter currentClientSetter) imapserver.Extension {
|
||||
if conExtID, ok := imapid.NewExtension(serverID).(imapserver.ConnExtension); ok {
|
||||
return &extension{
|
||||
extID: conExtID,
|
||||
setter: setter,
|
||||
extID: conExtID,
|
||||
clientSetter: clientSetter,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
|
||||
imapid "github.com/ProtonMail/go-imap-id"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/id"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
|
||||
@ -39,6 +40,7 @@ import (
|
||||
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"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -47,6 +49,7 @@ import (
|
||||
type imapServer struct {
|
||||
panicHandler panicHandler
|
||||
server *imapserver.Server
|
||||
userAgent *useragent.UserAgent
|
||||
eventListener listener.Listener
|
||||
debugClient bool
|
||||
debugServer bool
|
||||
@ -55,7 +58,7 @@ type imapServer struct {
|
||||
}
|
||||
|
||||
// NewIMAPServer constructs a new IMAP server configured with the given options.
|
||||
func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend *imapBackend, eventListener listener.Listener) *imapServer { //nolint[golint]
|
||||
func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend backend.Backend, userAgent *useragent.UserAgent, eventListener listener.Listener) *imapServer { // nolint[golint]
|
||||
s := imapserver.New(imapBackend)
|
||||
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
|
||||
s.TLSConfig = tls
|
||||
@ -93,7 +96,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
|
||||
s.Enable(
|
||||
imapidle.NewExtension(),
|
||||
imapmove.NewExtension(),
|
||||
id.NewExtension(serverID, imapBackend.bridge),
|
||||
id.NewExtension(serverID, userAgent),
|
||||
imapquota.NewExtension(),
|
||||
imapappendlimit.NewExtension(),
|
||||
imapunselect.NewExtension(),
|
||||
@ -103,6 +106,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
|
||||
server := &imapServer{
|
||||
panicHandler: panicHandler,
|
||||
server: s,
|
||||
userAgent: userAgent,
|
||||
eventListener: eventListener,
|
||||
debugClient: debugClient,
|
||||
debugServer: debugServer,
|
||||
@ -144,9 +148,10 @@ func (s *imapServer) listenAndServe(retries int) {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.server.Serve(&debugListener{
|
||||
Listener: l,
|
||||
server: s,
|
||||
err = s.server.Serve(&connListener{
|
||||
Listener: l,
|
||||
server: s,
|
||||
userAgent: s.userAgent,
|
||||
})
|
||||
// Serve returns error every time, even after closing the server.
|
||||
// User shouldn't be notified about error if server shouldn't be running,
|
||||
@ -233,18 +238,19 @@ func (s *imapServer) monitorDisconnectedUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
// debugListener sets debug loggers on server containing fields with local
|
||||
// connListener sets debug loggers on server containing fields with local
|
||||
// and remote addresses right after new connection is accepted.
|
||||
type debugListener struct {
|
||||
type connListener struct {
|
||||
net.Listener
|
||||
|
||||
server *imapServer
|
||||
server *imapServer
|
||||
userAgent *useragent.UserAgent
|
||||
}
|
||||
|
||||
func (dl *debugListener) Accept() (net.Conn, error) {
|
||||
conn, err := dl.Listener.Accept()
|
||||
func (l *connListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
|
||||
if err == nil && (dl.server.debugServer || dl.server.debugClient) {
|
||||
if err == nil && (l.server.debugServer || l.server.debugClient) {
|
||||
debugLog := log
|
||||
if addr := conn.LocalAddr(); addr != nil {
|
||||
debugLog = debugLog.WithField("loc", addr.String())
|
||||
@ -254,14 +260,18 @@ func (dl *debugListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
|
||||
var localDebug, remoteDebug io.Writer
|
||||
if dl.server.debugServer {
|
||||
if l.server.debugServer {
|
||||
localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel)
|
||||
}
|
||||
if dl.server.debugClient {
|
||||
if l.server.debugClient {
|
||||
remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
dl.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
|
||||
l.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
|
||||
}
|
||||
|
||||
if !l.userAgent.HasClient() {
|
||||
l.userAgent.SetClient("UnknownClient", "0.0.1")
|
||||
}
|
||||
|
||||
return conn, err
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
@ -48,6 +49,7 @@ func TestIMAPServerTurnOffAndOnAgain(t *testing.T) {
|
||||
panicHandler: panicHandler,
|
||||
server: server,
|
||||
eventListener: eventListener,
|
||||
userAgent: useragent.New(),
|
||||
}
|
||||
s.isRunning.Store(false)
|
||||
|
||||
|
||||
162
internal/sentry/reporter.go
Normal file
162
internal/sentry/reporter.go
Normal file
@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2021 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var skippedFunctions = []string{} //nolint[gochecknoglobals]
|
||||
|
||||
func init() { // nolint[noinit]
|
||||
if err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: constants.DSNSentry,
|
||||
Release: constants.Revision,
|
||||
BeforeSend: EnhanceSentryEvent,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("Failed to initialize sentry options")
|
||||
}
|
||||
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetFingerprint([]string{"{{ default }}"})
|
||||
})
|
||||
}
|
||||
|
||||
type Reporter struct {
|
||||
appName string
|
||||
appVersion string
|
||||
userAgent fmt.Stringer
|
||||
}
|
||||
|
||||
// NewReporter creates new sentry reporter with appName and appVersion to report.
|
||||
func NewReporter(appName, appVersion string, userAgent fmt.Stringer) *Reporter {
|
||||
return &Reporter{
|
||||
appName: appName,
|
||||
appVersion: appVersion,
|
||||
userAgent: userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) ReportException(i interface{}) error {
|
||||
err := fmt.Errorf("recover: %v", i)
|
||||
|
||||
return r.scopedReport(func() {
|
||||
if eventID := sentry.CaptureException(err); eventID != nil {
|
||||
logrus.WithError(err).
|
||||
WithField("reportID", *eventID).
|
||||
Warn("Captured exception")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Reporter) ReportMessage(msg string) error {
|
||||
return r.scopedReport(func() {
|
||||
if eventID := sentry.CaptureMessage(msg); eventID != nil {
|
||||
logrus.WithField("message", msg).
|
||||
WithField("reportID", *eventID).
|
||||
Warn("Captured message")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Report reports a sentry crash with stacktrace from all goroutines.
|
||||
func (r *Reporter) scopedReport(doReport func()) error {
|
||||
SkipDuringUnwind()
|
||||
|
||||
if os.Getenv("PROTONMAIL_ENV") == "dev" {
|
||||
return nil
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"OS": runtime.GOOS,
|
||||
"Client": r.appName,
|
||||
"Version": r.appVersion,
|
||||
"UserAgent": r.userAgent.String(),
|
||||
"UserID": "",
|
||||
}
|
||||
|
||||
sentry.WithScope(func(scope *sentry.Scope) {
|
||||
SkipDuringUnwind()
|
||||
scope.SetTags(tags)
|
||||
doReport()
|
||||
})
|
||||
|
||||
if !sentry.Flush(time.Second * 10) {
|
||||
return errors.New("failed to report sentry error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SkipDuringUnwind removes caller from the traceback.
|
||||
func SkipDuringUnwind() {
|
||||
pcs := make([]uintptr, 2)
|
||||
n := runtime.Callers(2, pcs)
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
frames := runtime.CallersFrames(pcs)
|
||||
frame, _ := frames.Next()
|
||||
if isFunctionFilteredOut(frame.Function) {
|
||||
return
|
||||
}
|
||||
|
||||
skippedFunctions = append(skippedFunctions, frame.Function)
|
||||
}
|
||||
|
||||
// EnhanceSentryEvent swaps type with value and removes panic handlers from the stacktrace.
|
||||
func EnhanceSentryEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
|
||||
for idx, exception := range event.Exception {
|
||||
exception.Type, exception.Value = exception.Value, exception.Type
|
||||
if exception.Stacktrace != nil {
|
||||
exception.Stacktrace.Frames = filterOutPanicHandlers(exception.Stacktrace.Frames)
|
||||
}
|
||||
event.Exception[idx] = exception
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func filterOutPanicHandlers(frames []sentry.Frame) []sentry.Frame {
|
||||
newFrames := []sentry.Frame{}
|
||||
for _, frame := range frames {
|
||||
// Sentry splits runtime.Frame.Function into Module and Function.
|
||||
function := frame.Module + "." + frame.Function
|
||||
if !isFunctionFilteredOut(function) {
|
||||
newFrames = append(newFrames, frame)
|
||||
}
|
||||
}
|
||||
return newFrames
|
||||
}
|
||||
|
||||
func isFunctionFilteredOut(function string) bool {
|
||||
for _, skipFunction := range skippedFunctions {
|
||||
if function == skipFunction {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
66
internal/sentry/reporter_test.go
Normal file
66
internal/sentry/reporter_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2021 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
r "github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
func TestSkipDuringUnwind(t *testing.T) {
|
||||
// More calls in one function adds it only once.
|
||||
SkipDuringUnwind()
|
||||
SkipDuringUnwind()
|
||||
func() {
|
||||
SkipDuringUnwind()
|
||||
SkipDuringUnwind()
|
||||
}()
|
||||
|
||||
wantSkippedFunctions := []string{
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry.TestSkipDuringUnwind",
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry.TestSkipDuringUnwind.func1",
|
||||
}
|
||||
r.Equal(t, wantSkippedFunctions, skippedFunctions)
|
||||
}
|
||||
|
||||
func TestFilterOutPanicHandlers(t *testing.T) {
|
||||
skippedFunctions = []string{
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config.(*PanicHandler).HandlePanic",
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config.HandlePanic",
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry.ReportSentryCrash",
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry.ReportSentryCrash.func1",
|
||||
}
|
||||
|
||||
frames := []sentry.Frame{
|
||||
{Module: "github.com/ProtonMail/proton-bridge/internal/cmd", Function: "main"},
|
||||
{Module: "github.com/urfave/cli", Function: "(*App).Run"},
|
||||
{Module: "github.com/ProtonMail/proton-bridge/internal/cmd", Function: "RegisterHandlePanic"},
|
||||
{Module: "github.com/ProtonMail/pkg", Function: "HandlePanic"},
|
||||
{Module: "main", Function: "run"},
|
||||
{Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "(*PanicHandler).HandlePanic"},
|
||||
{Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "HandlePanic"},
|
||||
{Module: "github.com/ProtonMail/proton-bridge/internal/sentry", Function: "ReportSentryCrash"},
|
||||
{Module: "github.com/ProtonMail/proton-bridge/internal/sentry", Function: "ReportSentryCrash.func1"},
|
||||
}
|
||||
|
||||
gotFrames := filterOutPanicHandlers(frames)
|
||||
r.Equal(t, frames[:5], gotFrames)
|
||||
}
|
||||
@ -24,9 +24,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
@ -188,18 +188,6 @@ func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
|
||||
}
|
||||
|
||||
// SetUserAgent mocks base method
|
||||
func (m *MockClientManager) SetUserAgent(arg0, arg1, arg2 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetUserAgent", arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SetUserAgent indicates an expected call of SetUserAgent
|
||||
func (mr *MockClientManagerMockRecorder) SetUserAgent(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserAgent", reflect.TypeOf((*MockClientManager)(nil).SetUserAgent), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// MockCredentialsStorer is a mock of CredentialsStorer interface
|
||||
type MockCredentialsStorer struct {
|
||||
ctrl *gomock.Controller
|
||||
|
||||
@ -55,7 +55,6 @@ type ClientManager interface {
|
||||
DisallowProxy()
|
||||
GetAuthUpdateChannel() chan pmapi.ClientAuth
|
||||
CheckConnection() error
|
||||
SetUserAgent(clientName, clientVersion, os string)
|
||||
}
|
||||
|
||||
type StoreMaker interface {
|
||||
|
||||
@ -26,12 +26,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
Reference in New Issue
Block a user