forked from Silverfish/proton-bridge
Merge release/golden-gate into devel
This commit is contained in:
@ -22,14 +22,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -41,21 +39,15 @@ var (
|
||||
|
||||
type apiServer struct {
|
||||
host string
|
||||
pref *config.Preferences
|
||||
tls *tls.Config
|
||||
certPath string
|
||||
keyPath string
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
}
|
||||
|
||||
// NewAPIServer returns prepared API server struct.
|
||||
func NewAPIServer(pref *config.Preferences, tls *tls.Config, certPath, keyPath string, eventListener listener.Listener) *apiServer { //nolint[golint]
|
||||
func NewAPIServer(settings *settings.Settings, eventListener listener.Listener) *apiServer { //nolint[golint]
|
||||
return &apiServer{
|
||||
host: bridge.Host,
|
||||
pref: pref,
|
||||
tls: tls,
|
||||
certPath: certPath,
|
||||
keyPath: keyPath,
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
}
|
||||
}
|
||||
@ -67,14 +59,12 @@ func (api *apiServer) ListenAndServe() {
|
||||
|
||||
addr := api.getAddress()
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
TLSConfig: api.tls,
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
log.Info("API listening at ", addr)
|
||||
if err := server.ListenAndServeTLS(api.certPath, api.keyPath); err != nil {
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
|
||||
log.Error("API failed: ", err)
|
||||
}
|
||||
@ -82,10 +72,10 @@ func (api *apiServer) ListenAndServe() {
|
||||
}
|
||||
|
||||
func (api *apiServer) getAddress() string {
|
||||
port := api.pref.GetInt(preferences.APIPortKey)
|
||||
port := api.settings.GetInt(settings.APIPortKey)
|
||||
newPort := ports.FindFreePortFrom(port)
|
||||
if newPort != port {
|
||||
api.pref.SetInt(preferences.APIPortKey, newPort)
|
||||
api.settings.SetInt(settings.APIPortKey, newPort)
|
||||
}
|
||||
return getAPIAddress(api.host, newPort)
|
||||
}
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
@ -37,12 +36,9 @@ func focusHandler(ctx handlerContext) error {
|
||||
|
||||
// CheckOtherInstanceAndFocus is helper for new instances to check if there is
|
||||
// already a running instance and get it's focus.
|
||||
func CheckOtherInstanceAndFocus(port int, tls *tls.Config) error {
|
||||
transport := &http.Transport{TLSClientConfig: tls}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
func CheckOtherInstanceAndFocus(port int) error {
|
||||
addr := getAPIAddress(bridge.Host, port)
|
||||
resp, err := client.Get("https://" + addr + "/focus")
|
||||
resp, err := (&http.Client{}).Get("http://" + addr + "/focus")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -15,21 +15,21 @@
|
||||
// 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 cmd
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
import "strings"
|
||||
|
||||
// filterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber
|
||||
// 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 filterProcessSerialNumberFromArgs() {
|
||||
tmp := os.Args[:0]
|
||||
for _, arg := range os.Args {
|
||||
func StripProcessSerialNumber(args []string) []string {
|
||||
res := args[:0]
|
||||
|
||||
for _, arg := range args {
|
||||
if !strings.Contains(arg, "-psn_") {
|
||||
tmp = append(tmp, arg)
|
||||
res = append(res, arg)
|
||||
}
|
||||
}
|
||||
os.Args = tmp
|
||||
|
||||
return res
|
||||
}
|
||||
357
internal/app/base/base.go
Normal file
357
internal/app/base/base.go
Normal file
@ -0,0 +1,357 @@
|
||||
// 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 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 ClientManager
|
||||
// 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 (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||
"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/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/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"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
CrashHandler *crash.Handler
|
||||
Locations *locations.Locations
|
||||
Settings *settings.Settings
|
||||
Lock *os.File
|
||||
Cache *cache.Cache
|
||||
Listener listener.Listener
|
||||
Creds *credentials.Store
|
||||
CM *pmapi.ClientManager
|
||||
CookieJar *cookies.Jar
|
||||
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
|
||||
|
||||
teardown []func() error // actions to perform when app is exiting
|
||||
}
|
||||
|
||||
func New( // nolint[funlen]
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion string,
|
||||
) (*Base, error) {
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version)
|
||||
|
||||
crashHandler := crash.NewHandler(
|
||||
sentryReporter.ReportException,
|
||||
crash.ShowErrorNotification(appName),
|
||||
)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
if err := locations.Clean(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
settingsPath, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settingsObj := settings.New(settingsPath)
|
||||
|
||||
lock, err := singleinstance.CreateLockFile(locations.GetLockFile())
|
||||
if err != nil {
|
||||
logrus.Warnf("%v is already running", appName)
|
||||
return nil, api.CheckOtherInstanceAndFocus(settingsObj.GetInt(settings.APIPortKey))
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
jar, err := cookies.NewCookieJar(settingsObj)
|
||||
if err != nil {
|
||||
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.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
|
||||
cm.SetCookieJar(jar)
|
||||
sentryReporter.SetUserAgentProvider(cm)
|
||||
|
||||
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: appName,
|
||||
DisplayName: appName,
|
||||
Exec: []string{exe},
|
||||
}
|
||||
|
||||
return &Base{
|
||||
CrashHandler: crashHandler,
|
||||
Locations: locations,
|
||||
Settings: settingsObj,
|
||||
Lock: lock,
|
||||
Cache: cache,
|
||||
Listener: listener,
|
||||
Creds: credentials.NewStore(kc),
|
||||
CM: cm,
|
||||
CookieJar: jar,
|
||||
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,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *Base) NewApp(action 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.run(action)
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "cpu-prof",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "mem-prof",
|
||||
Aliases: []string{"m"},
|
||||
Usage: "Generate memory profile",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "cli",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "Use command line interface",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "restart",
|
||||
Usage: "The number of times the application has already restarted",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "launcher",
|
||||
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
|
||||
}
|
||||
|
||||
// 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) run(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/autostart.
|
||||
if launcher := c.String("launcher"); launcher != "" {
|
||||
b.Autostart.Exec = []string{launcher}
|
||||
b.command = launcher
|
||||
}
|
||||
|
||||
if doCPUProfile := c.Bool("cpu-prof"); doCPUProfile {
|
||||
startCPUProfile()
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if doMemoryProfile := c.Bool("mem-prof"); doMemoryProfile {
|
||||
defer makeMemoryProfile()
|
||||
}
|
||||
|
||||
logging.SetLevel(c.String("log-level"))
|
||||
|
||||
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 {
|
||||
if c.Int("restart") > maxAllowedRestarts {
|
||||
logrus.
|
||||
WithField("restart", c.Int("restart")).
|
||||
Warn("Not restarting, already restarted too many times")
|
||||
|
||||
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
|
||||
}
|
||||
83
internal/app/base/migration.go
Normal file
83
internal/app/base/migration.go
Normal file
@ -0,0 +1,83 @@
|
||||
// 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 base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/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/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
|
||||
// | c11 | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
|
||||
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()
|
||||
newSettingsDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveIfExists(
|
||||
filepath.Join(userCacheDir, "c11", "prefs.json"),
|
||||
filepath.Join(newSettingsDir, "prefs.json"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newCacheDir, err := locations.ProvideCachePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveIfExists(
|
||||
filepath.Join(userCacheDir, "c11"),
|
||||
filepath.Join(newCacheDir, "c11"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveIfExists(source, destination string) error {
|
||||
if _, err := os.Stat(source); os.IsNotExist(err) {
|
||||
logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file")
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(destination); !os.IsNotExist(err) {
|
||||
logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file")
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
@ -15,40 +15,42 @@
|
||||
// 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 cmd
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StartCPUProfile starts CPU pprof.
|
||||
func StartCPUProfile() {
|
||||
// startCPUProfile starts CPU pprof.
|
||||
func startCPUProfile() {
|
||||
f, err := os.Create("./cpu.pprof")
|
||||
if err != nil {
|
||||
log.Fatal("Could not create CPU profile: ", err)
|
||||
logrus.Fatal("Could not create CPU profile: ", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("Could not start CPU profile: ", err)
|
||||
logrus.Fatal("Could not start CPU profile: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// MakeMemoryProfile generates memory pprof.
|
||||
func MakeMemoryProfile() {
|
||||
// makeMemoryProfile generates memory pprof.
|
||||
func makeMemoryProfile() {
|
||||
name := "./mem.pprof"
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
log.Fatal("Could not create memory profile: ", err)
|
||||
logrus.Fatal("Could not create memory profile: ", err)
|
||||
}
|
||||
if abs, err := filepath.Abs(name); err == nil {
|
||||
name = abs
|
||||
}
|
||||
log.Info("Writing memory profile to ", name)
|
||||
logrus.Info("Writing memory profile to ", name)
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
log.Fatal("Could not write memory profile: ", err)
|
||||
logrus.Fatal("Could not write memory profile: ", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
}
|
||||
79
internal/app/base/restart.go
Normal file
79
internal/app/base/restart.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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 base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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:]
|
||||
} else {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
|
||||
logrus.
|
||||
WithField("command", b.command).
|
||||
WithField("args", args).
|
||||
Warn("Restarting")
|
||||
|
||||
return exec.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
|
||||
}
|
||||
49
internal/app/base/restart_test.go
Normal file
49
internal/app/base/restart_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 base
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIncrementRestartFlag(t *testing.T) {
|
||||
var 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
224
internal/app/bridge/bridge.go
Normal file
224
internal/app/bridge/bridge.go
Normal file
@ -0,0 +1,224 @@
|
||||
// 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 bridge implements the bridge CLI application.
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
pkgTLS "github.com/ProtonMail/proton-bridge/internal/config/tls"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap"
|
||||
"github.com/ProtonMail/proton-bridge/internal/smtp"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func New(base *base.Base) *cli.App {
|
||||
app := base.NewApp(run)
|
||||
|
||||
app.Flags = append(app.Flags, []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "log-imap",
|
||||
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"},
|
||||
&cli.BoolFlag{
|
||||
Name: "log-smtp",
|
||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)"},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-window",
|
||||
Usage: "Don't show window after start"},
|
||||
&cli.BoolFlag{
|
||||
Name: "noninteractive",
|
||||
Usage: "Start Bridge entirely noninteractively"},
|
||||
}...)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
tlsConfig, err := loadTLSConfig(b)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to load TLS config")
|
||||
}
|
||||
|
||||
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds)
|
||||
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, bridge)
|
||||
smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge)
|
||||
|
||||
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("log-imap") == "client" || c.String("log-imap") == "all",
|
||||
c.String("log-imap") == "server" || c.String("log-imap") == "all",
|
||||
imapPort, tlsConfig, imapBackend, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
|
||||
useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
|
||||
smtp.NewSMTPServer(
|
||||
c.Bool("log-smtp"),
|
||||
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
// Bridge supports no-window option which we should use for autostart.
|
||||
b.Autostart.Exec = append(b.Autostart.Exec, "--no-window")
|
||||
|
||||
// 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("cli"):
|
||||
frontendMode = "cli"
|
||||
case c.Bool("noninteractive"):
|
||||
return <-(make(chan error)) // Block forever.
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
}
|
||||
|
||||
f := frontend.New(
|
||||
constants.Version,
|
||||
constants.BuildVersion,
|
||||
b.Name,
|
||||
frontendMode,
|
||||
!c.Bool("no-window"),
|
||||
b.CrashHandler,
|
||||
b.Locations,
|
||||
b.Settings,
|
||||
b.Listener,
|
||||
b.Updater,
|
||||
bridge,
|
||||
smtpBackend,
|
||||
b.Autostart,
|
||||
b,
|
||||
)
|
||||
|
||||
// Watch for updates routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
|
||||
for {
|
||||
checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey))
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
|
||||
return f.Loop()
|
||||
}
|
||||
|
||||
func loadTLSConfig(b *base.Base) (*tls.Config, error) {
|
||||
if !b.TLS.HasCerts() {
|
||||
if err := generateTLSCerts(b); 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 := generateTLSCerts(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.TLS.GetConfig()
|
||||
}
|
||||
|
||||
func generateTLSCerts(b *base.Base) 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 checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
|
||||
version, err := u.Check()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("An error occurred while checking for updates")
|
||||
return
|
||||
}
|
||||
|
||||
// Update links in UI
|
||||
f.SetVersion(version)
|
||||
|
||||
if !u.IsUpdateApplicable(version) {
|
||||
logrus.Debug("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("version", version.Version).Info("An update is available")
|
||||
|
||||
if !autoUpdate {
|
||||
f.NotifyManualUpdate(version, u.CanInstall(version))
|
||||
return
|
||||
}
|
||||
|
||||
if !u.CanInstall(version) {
|
||||
logrus.Info("A manual update is required")
|
||||
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.InstallUpdate(version); err != nil {
|
||||
if errors.Cause(err) == updater.ErrDownloadVerify {
|
||||
logrus.WithError(err).Warning("Skipping update installation due to temporary error")
|
||||
} else {
|
||||
logrus.WithError(err).Error("The update couldn't be installed")
|
||||
f.NotifySilentUpdateError(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f.NotifySilentUpdateInstalled()
|
||||
}
|
||||
131
internal/app/ie/ie.go
Normal file
131
internal/app/ie/ie.go
Normal file
@ -0,0 +1,131 @@
|
||||
// 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 ie implements the ie CLI application.
|
||||
package ie
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func New(b *base.Base) *cli.App {
|
||||
return b.NewApp(run)
|
||||
}
|
||||
|
||||
func run(b *base.Base, c *cli.Context) error {
|
||||
ie := importexport.New(b.Locations, b.Cache, b.CrashHandler, b.Listener, b.CM, b.Creds)
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
api.NewAPIServer(b.Settings, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case c.Bool("cli"):
|
||||
frontendMode = "cli"
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
f := frontend.NewImportExport(
|
||||
constants.Version,
|
||||
constants.BuildVersion,
|
||||
b.Name,
|
||||
frontendMode,
|
||||
b.CrashHandler,
|
||||
b.Locations,
|
||||
b.Settings,
|
||||
b.Listener,
|
||||
b.Updater,
|
||||
ie,
|
||||
b,
|
||||
)
|
||||
|
||||
// Watch for updates routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
|
||||
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) {
|
||||
version, err := u.Check()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("An error occurred while checking for updates")
|
||||
return
|
||||
}
|
||||
|
||||
// Update links in UI
|
||||
f.SetVersion(version)
|
||||
|
||||
if !u.IsUpdateApplicable(version) {
|
||||
logrus.Debug("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("version", version.Version).Info("An update is available")
|
||||
|
||||
if !autoUpdate {
|
||||
f.NotifyManualUpdate(version, u.CanInstall(version))
|
||||
return
|
||||
}
|
||||
|
||||
if !u.CanInstall(version) {
|
||||
logrus.Info("A manual update is required")
|
||||
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.InstallUpdate(version); err != nil {
|
||||
if errors.Cause(err) == updater.ErrDownloadVerify {
|
||||
logrus.WithError(err).Warning("Skipping update installation due to temporary error")
|
||||
} else {
|
||||
logrus.WithError(err).Error("The update couldn't be installed")
|
||||
f.NotifySilentUpdateError(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f.NotifySilentUpdateInstalled()
|
||||
}
|
||||
@ -19,11 +19,13 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
|
||||
@ -38,7 +40,7 @@ var (
|
||||
type Bridge struct {
|
||||
*users.Users
|
||||
|
||||
pref PreferenceProvider
|
||||
settings SettingsProvider
|
||||
clientManager users.ClientManager
|
||||
|
||||
userAgentClientName string
|
||||
@ -47,8 +49,9 @@ type Bridge struct {
|
||||
}
|
||||
|
||||
func New(
|
||||
config Configer,
|
||||
pref PreferenceProvider,
|
||||
locations Locator,
|
||||
cache Cacher,
|
||||
s SettingsProvider,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
clientManager users.ClientManager,
|
||||
@ -56,22 +59,25 @@ func New(
|
||||
) *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 pref.GetBool(preferences.AllowProxyKey) {
|
||||
if s.GetBool(settings.AllowProxyKey) {
|
||||
clientManager.AllowProxy()
|
||||
}
|
||||
|
||||
storeFactory := newStoreFactory(config, panicHandler, clientManager, eventListener)
|
||||
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
|
||||
storeFactory := newStoreFactory(cache, panicHandler, clientManager, eventListener)
|
||||
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
|
||||
b := &Bridge{
|
||||
Users: u,
|
||||
|
||||
pref: pref,
|
||||
settings: s,
|
||||
clientManager: clientManager,
|
||||
}
|
||||
|
||||
if pref.GetBool(preferences.FirstStartKey) {
|
||||
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
|
||||
pref.SetBool(preferences.FirstStartKey, false)
|
||||
if s.GetBool(settings.FirstStartKey) {
|
||||
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
|
||||
s.SetBool(settings.FirstStartKey, false)
|
||||
}
|
||||
|
||||
go b.heartbeat()
|
||||
@ -81,19 +87,25 @@ func New(
|
||||
|
||||
// heartbeat sends a heartbeat signal once a day.
|
||||
func (b *Bridge) heartbeat() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
|
||||
for range ticker.C {
|
||||
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
|
||||
for range time.Tick(time.Minute) {
|
||||
lastHeartbeatDay, err := strconv.ParseInt(b.settings.Get(settings.LastHeartbeatKey), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
nextTime := time.Unix(next, 0)
|
||||
if time.Now().After(nextTime) {
|
||||
b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
|
||||
nextTime = nextTime.Add(24 * time.Hour)
|
||||
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
|
||||
|
||||
// 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()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,6 +134,12 @@ func (b *Bridge) SetCurrentOS(os string) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at Mon Dec 28 02:39:43 PM CET 2020. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Fri Jan 22 11:28:55 CET 2021. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;github.com/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
|
||||
@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
type storeFactory struct {
|
||||
config StoreFactoryConfiger
|
||||
cache Cacher
|
||||
panicHandler users.PanicHandler
|
||||
clientManager users.ClientManager
|
||||
eventListener listener.Listener
|
||||
@ -36,29 +36,29 @@ type storeFactory struct {
|
||||
}
|
||||
|
||||
func newStoreFactory(
|
||||
config StoreFactoryConfiger,
|
||||
cache Cacher,
|
||||
panicHandler users.PanicHandler,
|
||||
clientManager users.ClientManager,
|
||||
eventListener listener.Listener,
|
||||
) *storeFactory {
|
||||
return &storeFactory{
|
||||
config: config,
|
||||
cache: cache,
|
||||
panicHandler: panicHandler,
|
||||
clientManager: clientManager,
|
||||
eventListener: eventListener,
|
||||
storeCache: store.NewCache(config.GetIMAPCachePath()),
|
||||
storeCache: store.NewCache(cache.GetIMAPCachePath()),
|
||||
}
|
||||
}
|
||||
|
||||
// New creates new store for given user.
|
||||
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
|
||||
storePath := getUserStorePath(f.config.GetDBDir(), user.ID())
|
||||
storePath := getUserStorePath(f.cache.GetDBDir(), user.ID())
|
||||
return store.New(f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache)
|
||||
}
|
||||
|
||||
// Remove removes all store files for given user.
|
||||
func (f *storeFactory) Remove(userID string) error {
|
||||
storePath := getUserStorePath(f.config.GetDBDir(), userID)
|
||||
storePath := getUserStorePath(f.cache.GetDBDir(), userID)
|
||||
return store.RemoveStore(f.storeCache, storePath, userID)
|
||||
}
|
||||
|
||||
|
||||
@ -17,22 +17,18 @@
|
||||
|
||||
package bridge
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/internal/users"
|
||||
|
||||
type Configer interface {
|
||||
users.Configer
|
||||
StoreFactoryConfiger
|
||||
type Locator interface {
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type StoreFactoryConfiger interface {
|
||||
GetDBDir() string
|
||||
type Cacher interface {
|
||||
GetIMAPCachePath() string
|
||||
GetDBDir() string
|
||||
}
|
||||
|
||||
type PreferenceProvider interface {
|
||||
type SettingsProvider interface {
|
||||
Get(key string) string
|
||||
Set(key string, value string)
|
||||
GetBool(key string) bool
|
||||
SetBool(key string, val bool)
|
||||
GetInt(key string) int
|
||||
Set(key string, value string)
|
||||
}
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
pkgSentry "github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "cmd") //nolint[gochecknoglobals]
|
||||
|
||||
baseFlags = []cli.Flag{ //nolint[gochecknoglobals]
|
||||
cli.StringFlag{
|
||||
Name: "log-level, l",
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
|
||||
cli.BoolFlag{
|
||||
Name: "cli, c",
|
||||
Usage: "Use command line interface"},
|
||||
cli.StringFlag{
|
||||
Name: "version-json, g",
|
||||
Usage: "Generate json version file"},
|
||||
cli.BoolFlag{
|
||||
Name: "mem-prof, m",
|
||||
Usage: "Generate memory profile"},
|
||||
cli.BoolFlag{
|
||||
Name: "cpu-prof, p",
|
||||
Usage: "Generate CPU profile"},
|
||||
}
|
||||
)
|
||||
|
||||
// Main sets up Sentry, filters out unwanted args, creates app and runs it.
|
||||
func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: constants.DSNSentry,
|
||||
Release: constants.Revision,
|
||||
BeforeSend: pkgSentry.EnhanceSentryEvent,
|
||||
})
|
||||
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetFingerprint([]string{"{{ default }}"})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Errorln("Can not setup sentry DSN")
|
||||
}
|
||||
|
||||
filterProcessSerialNumberFromArgs()
|
||||
filterRestartNumberFromArgs()
|
||||
|
||||
app := newApp(appName, usage, extraFlags, run)
|
||||
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
log.WithField("version", constants.Version).
|
||||
WithField("revision", constants.Revision).
|
||||
WithField("build", constants.BuildTime).
|
||||
WithField("runtime", runtime.GOOS).
|
||||
WithField("args", os.Args).
|
||||
WithField("appName", app.Name).
|
||||
Info("Run app")
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Error("Program exited with error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newApp(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = appName
|
||||
app.Usage = usage
|
||||
app.Version = constants.BuildVersion
|
||||
app.Flags = append(baseFlags, extraFlags...) //nolint[gocritic]
|
||||
app.Action = run
|
||||
return app
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
// After how many crashes app gives up starting.
|
||||
maxAllowedCrashes = 10
|
||||
)
|
||||
|
||||
var (
|
||||
// How many crashes happened so far in a row.
|
||||
// It will be filled from args by `filterRestartNumberFromArgs`.
|
||||
// Every call of `HandlePanic` will increase this number.
|
||||
// Then it will be passed as argument to the next try by `RestartApp`.
|
||||
numberOfCrashes = 0 //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
// filterRestartNumberFromArgs removes flag with a number how many restart we already did.
|
||||
// See restartApp how that number is used.
|
||||
func filterRestartNumberFromArgs() {
|
||||
tmp := os.Args[:0]
|
||||
for i, arg := range os.Args {
|
||||
if !strings.HasPrefix(arg, "--restart_") {
|
||||
tmp = append(tmp, arg)
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
numberOfCrashes, err = strconv.Atoi(os.Args[i][10:])
|
||||
if err != nil {
|
||||
numberOfCrashes = maxAllowedCrashes
|
||||
}
|
||||
}
|
||||
os.Args = tmp
|
||||
}
|
||||
|
||||
// DisableRestart disables restart once `RestartApp` is called.
|
||||
func DisableRestart() {
|
||||
numberOfCrashes = maxAllowedCrashes
|
||||
}
|
||||
|
||||
// RestartApp starts a new instance in background.
|
||||
func RestartApp() {
|
||||
if numberOfCrashes >= maxAllowedCrashes {
|
||||
log.Error("Too many crashes")
|
||||
return
|
||||
}
|
||||
if exeFile, err := os.Executable(); err == nil {
|
||||
arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes))
|
||||
cmd := exec.Command(exeFile, arguments...) //nolint[gosec]
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Error("Restart failed: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PanicHandler defines HandlePanic which can be used anywhere in defer.
|
||||
type PanicHandler struct {
|
||||
AppName string
|
||||
Config *config.Config
|
||||
Err *error // Pointer to error of cli action.
|
||||
}
|
||||
|
||||
// HandlePanic should be called in defer to ensure restart of app after error.
|
||||
func (ph *PanicHandler) HandlePanic() {
|
||||
sentry.SkipDuringUnwind()
|
||||
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
config.HandlePanic(ph.Config, fmt.Sprintf("Recover: %v", r))
|
||||
frontend.HandlePanic(ph.AppName)
|
||||
|
||||
*ph.Err = cli.NewExitError("Panic and restart", 255)
|
||||
numberOfCrashes++
|
||||
log.Error("Restarting after panic")
|
||||
RestartApp()
|
||||
os.Exit(255)
|
||||
}
|
||||
65
internal/config/cache/cache.go
vendored
Normal file
65
internal/config/cache/cache.go
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
// 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 cache provides access to contents inside a cache directory.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/files"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
dir, version string
|
||||
}
|
||||
|
||||
func New(dir, version string) (*Cache, error) {
|
||||
if err := os.MkdirAll(filepath.Join(dir, version), 0700); 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()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
70
internal/config/cache/cache_test.go
vendored
Normal file
70
internal/config/cache/cache_test.go
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
// 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 cache
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRemoveOldVersions(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "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)), 0700))
|
||||
|
||||
f, err := os.Create(filepath.Join(dir, target))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
}
|
||||
}
|
||||
142
internal/config/settings/kvs.go
Normal file
142
internal/config/settings/kvs.go
Normal file
@ -0,0 +1,142 @@
|
||||
// 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 settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type keyValueStore struct {
|
||||
cache map[string]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.cache != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
p.cache = map[string]string{}
|
||||
|
||||
f, err := os.Open(p.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() //nolint[errcheck]
|
||||
|
||||
return json.NewDecoder(f).Decode(&p.cache)
|
||||
}
|
||||
|
||||
func (p *keyValueStore) save() error {
|
||||
if p.cache == nil {
|
||||
return errors.New("cannot save preferences: cache is nil")
|
||||
}
|
||||
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
f, err := os.Create(p.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() //nolint[errcheck]
|
||||
|
||||
return json.NewEncoder(f).Encode(p.cache)
|
||||
}
|
||||
|
||||
func (p *keyValueStore) setDefault(key, value string) {
|
||||
if p.Get(key) == "" {
|
||||
p.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) Get(key string) string {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
return p.cache[key]
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetBool(key string) bool {
|
||||
return p.Get(key) == "true"
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetInt(key string) int {
|
||||
value, err := strconv.Atoi(p.Get(key))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Cannot parse int")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *keyValueStore) GetFloat64(key string) float64 {
|
||||
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, value string) {
|
||||
p.lock.Lock()
|
||||
p.cache[key] = value
|
||||
p.lock.Unlock()
|
||||
|
||||
if err := p.save(); err != nil {
|
||||
logrus.WithError(err).Warn("Cannot save preferences")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetBool(key string, value bool) {
|
||||
if value {
|
||||
p.Set(key, "true")
|
||||
} else {
|
||||
p.Set(key, "false")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetInt(key string, value int) {
|
||||
p.Set(key, strconv.Itoa(value))
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetFloat64(key string, value float64) {
|
||||
p.Set(key, fmt.Sprintf("%v", value))
|
||||
}
|
||||
105
internal/config/settings/kvs_test.go
Normal file
105
internal/config/settings/kvs_test.go
Normal file
@ -0,0 +1,105 @@
|
||||
// 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 settings
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testPrefFilePath = "/tmp/pref.json"
|
||||
|
||||
func TestLoadNoKeyValueStore(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
require.Equal(t, "", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestLoadBadKeyValueStore(t *testing.T) {
|
||||
require.NoError(t, ioutil.WriteFile(testPrefFilePath, []byte("{\"key\":\"value"), 0700))
|
||||
pref := newKeyValueStore(testPrefFilePath)
|
||||
require.Equal(t, "", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreGet(t *testing.T) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, "value", pref.Get("str"))
|
||||
require.Equal(t, "42", pref.Get("int"))
|
||||
require.Equal(t, "true", pref.Get("bool"))
|
||||
require.Equal(t, "t", pref.Get("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreGetInt(t *testing.T) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, 0, pref.GetInt("str"))
|
||||
require.Equal(t, 42, pref.GetInt("int"))
|
||||
require.Equal(t, 0, pref.GetInt("bool"))
|
||||
require.Equal(t, 0, pref.GetInt("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreGetBool(t *testing.T) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, false, pref.GetBool("str"))
|
||||
require.Equal(t, false, pref.GetBool("int"))
|
||||
require.Equal(t, true, pref.GetBool("bool"))
|
||||
require.Equal(t, false, pref.GetBool("falseBool"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetDefault(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.setDefault("key", "value")
|
||||
pref.setDefault("key", "othervalue")
|
||||
require.Equal(t, "value", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSet(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.Set("str", "value")
|
||||
checkSavedKeyValueStore(t, "{\"str\":\"value\"}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetInt(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.SetInt("int", 42)
|
||||
checkSavedKeyValueStore(t, "{\"int\":\"42\"}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetBool(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.SetBool("trueBool", true)
|
||||
pref.SetBool("falseBool", false)
|
||||
checkSavedKeyValueStore(t, "{\"falseBool\":\"false\",\"trueBool\":\"true\"}")
|
||||
}
|
||||
|
||||
func newTestEmptyKeyValueStore(t *testing.T) *keyValueStore {
|
||||
require.NoError(t, os.RemoveAll(testPrefFilePath))
|
||||
return newKeyValueStore(testPrefFilePath)
|
||||
}
|
||||
|
||||
func newTestKeyValueStore(t *testing.T) *keyValueStore {
|
||||
require.NoError(t, ioutil.WriteFile(testPrefFilePath, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0700))
|
||||
return newKeyValueStore(testPrefFilePath)
|
||||
}
|
||||
|
||||
func checkSavedKeyValueStore(t *testing.T, expected string) {
|
||||
data, err := ioutil.ReadFile(testPrefFilePath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected+"\n", string(data))
|
||||
}
|
||||
90
internal/config/settings/settings.go
Normal file
90
internal/config/settings/settings.go
Normal file
@ -0,0 +1,90 @@
|
||||
// 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 settings provides access to persistent user settings.
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Keys of preferences in JSON file.
|
||||
const (
|
||||
FirstStartKey = "first_time_start"
|
||||
FirstStartGUIKey = "first_time_start_gui"
|
||||
LastHeartbeatKey = "last_heartbeat"
|
||||
APIPortKey = "user_port_api"
|
||||
IMAPPortKey = "user_port_imap"
|
||||
SMTPPortKey = "user_port_smtp"
|
||||
SMTPSSLKey = "user_ssl_smtp"
|
||||
AllowProxyKey = "allow_proxy"
|
||||
AutostartKey = "autostart"
|
||||
AutoUpdateKey = "autoupdate"
|
||||
CookiesKey = "cookies"
|
||||
ReportOutgoingNoEncKey = "report_outgoing_email_without_encryption"
|
||||
LastVersionKey = "last_used_version"
|
||||
UpdateChannelKey = "update_channel"
|
||||
RolloutKey = "rollout"
|
||||
PreferredKeychainKey = "preferred_keychain"
|
||||
)
|
||||
|
||||
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(ReportOutgoingNoEncKey, "false")
|
||||
s.setDefault(LastVersionKey, "")
|
||||
s.setDefault(UpdateChannelKey, "")
|
||||
s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64()))
|
||||
s.setDefault(PreferredKeychainKey, "")
|
||||
|
||||
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")
|
||||
}
|
||||
53
internal/config/tls/cert_store_darwin.go
Normal file
53
internal/config/tls/cert_store_darwin.go
Normal file
@ -0,0 +1,53 @@
|
||||
// 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 tls
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func addTrustedCert(certPath string) error {
|
||||
return exec.Command( // nolint[gosec]
|
||||
"/usr/bin/security",
|
||||
"execute-with-privileges",
|
||||
"/usr/bin/security",
|
||||
"add-trusted-cert",
|
||||
"-d",
|
||||
"-r", "trustRoot",
|
||||
"-p", "ssl",
|
||||
"-k", "/Library/Keychains/System.keychain",
|
||||
certPath,
|
||||
).Run()
|
||||
}
|
||||
|
||||
func removeTrustedCert(certPath string) error {
|
||||
return exec.Command( // nolint[gosec]
|
||||
"/usr/bin/security",
|
||||
"execute-with-privileges",
|
||||
"/usr/bin/security",
|
||||
"remove-trusted-cert",
|
||||
"-d",
|
||||
certPath,
|
||||
).Run()
|
||||
}
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
return addTrustedCert(t.getTLSCertPath())
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
return removeTrustedCert(t.getTLSCertPath())
|
||||
}
|
||||
26
internal/config/tls/cert_store_linux.go
Normal file
26
internal/config/tls/cert_store_linux.go
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 tls
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
@ -15,14 +15,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./release-notes.sh at 'Mon Dec 28 02:39:43 PM CET 2020'. DO NOT EDIT.
|
||||
package tls
|
||||
|
||||
package importexport
|
||||
func (t *TLS) InstallCerts() error {
|
||||
return nil // NOTE(GODT-986): Install certs to root cert store?
|
||||
}
|
||||
|
||||
const ReleaseNotes = `• Allow an import of already encrypted messages (as cypher text)
|
||||
• Cosmetic GUI changes
|
||||
• Better error handling
|
||||
`
|
||||
|
||||
const ReleaseFixedBugs = `• Installation issues on linux
|
||||
`
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||
}
|
||||
158
internal/config/tls/tls.go
Normal file
158
internal/config/tls/tls.go
Normal file
@ -0,0 +1,158 @@
|
||||
// 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 tls
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"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,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTLSTemplate creates a new TLS template certificate with a random serial number.
|
||||
func NewTLSTemplate() (*x509.Certificate, error) {
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate serial number")
|
||||
}
|
||||
|
||||
return &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"CH"},
|
||||
Organization: []string{"Proton Technologies AG"},
|
||||
OrganizationalUnit: []string{"ProtonMail"},
|
||||
CommonName: "127.0.0.1",
|
||||
},
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(20 * 365 * 24 * time.Hour),
|
||||
}, 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 := os.Stat(t.getTLSKeyPath()); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
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, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close() // nolint[errcheck]
|
||||
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return 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())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load keypair")
|
||||
}
|
||||
|
||||
c.Leaf, err = x509.ParseCertificate(c.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse certificate")
|
||||
}
|
||||
|
||||
if time.Now().Add(31 * 24 * time.Hour).After(c.Leaf.NotAfter) {
|
||||
return nil, ErrTLSCertExpiresSoon
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(c.Leaf)
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{c},
|
||||
ServerName: "127.0.0.1",
|
||||
ClientAuth: tls.VerifyClientCertIfGiven,
|
||||
RootCAs: caCertPool,
|
||||
ClientCAs: caCertPool,
|
||||
}, nil
|
||||
}
|
||||
77
internal/config/tls/tls_test.go
Normal file
77
internal/config/tls/tls_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
// 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 tls
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetOldConfig(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-tls")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new tls object.
|
||||
tls := New(dir)
|
||||
|
||||
// Create new TLS template.
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make the template be an old key.
|
||||
tlsTemplate.NotBefore = time.Now().Add(-365 * 24 * time.Hour)
|
||||
tlsTemplate.NotAfter = time.Now()
|
||||
|
||||
// Generate the certs from the template.
|
||||
require.NoError(t, tls.GenerateCerts(tlsTemplate))
|
||||
|
||||
// Generate the config from the certs -- it's going to expire soon so we don't want to use it.
|
||||
_, err = tls.GetConfig()
|
||||
require.Equal(t, err, ErrTLSCertExpiresSoon)
|
||||
}
|
||||
|
||||
func TestGetValidConfig(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-tls")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new tls object.
|
||||
tls := New(dir)
|
||||
|
||||
// Create new TLS template.
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make the template be a new key.
|
||||
tlsTemplate.NotBefore = time.Now()
|
||||
tlsTemplate.NotAfter = time.Now().Add(2 * 365 * 24 * time.Hour)
|
||||
|
||||
// Generate the certs from the template.
|
||||
require.NoError(t, tls.GenerateCerts(tlsTemplate))
|
||||
|
||||
// Generate the config from the certs -- it's not going to expire soon so we want to use it.
|
||||
config, err := tls.GetConfig()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(config.Certificates), 1)
|
||||
|
||||
// Check the cert is valid.
|
||||
now, notValidAfter := time.Now(), config.Certificates[0].Leaf.NotAfter
|
||||
require.False(t, now.After(notValidAfter), "new certificate expected to be valid at %v but have valid until %v", now, notValidAfter)
|
||||
}
|
||||
41
internal/constants/constants.go
Normal file
41
internal/constants/constants.go
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.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 constants contains variables that are set via ldflags during build.
|
||||
package constants
|
||||
|
||||
import "fmt"
|
||||
|
||||
const VendorName = "protonmail"
|
||||
|
||||
// nolint[gochecknoglobals]
|
||||
var (
|
||||
// Version of the build.
|
||||
Version = ""
|
||||
|
||||
// Revision is current hash of the build.
|
||||
Revision = ""
|
||||
|
||||
// BuildTime stamp of the build.
|
||||
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)
|
||||
)
|
||||
@ -19,46 +19,41 @@
|
||||
package cookies
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
)
|
||||
|
||||
type cookiesByHost map[string][]*http.Cookie
|
||||
|
||||
// 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
|
||||
pantry *pantry
|
||||
locker sync.Locker
|
||||
jar *cookiejar.Jar
|
||||
settings *settings.Settings
|
||||
cookies cookiesByHost
|
||||
locker sync.Locker
|
||||
}
|
||||
|
||||
type GetterSetter interface {
|
||||
Get(string) string
|
||||
Set(string, string)
|
||||
}
|
||||
|
||||
func NewCookieJar(gs GetterSetter) (*Jar, error) {
|
||||
pantry := &pantry{gs: gs}
|
||||
|
||||
if err := pantry.discardExpiredCookies(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cookies, err := pantry.loadFromJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func NewCookieJar(s *settings.Settings) (*Jar, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rawURL, cookies := range cookies {
|
||||
url, err := url.Parse(rawURL)
|
||||
cookiesByHost, err := loadCookies(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for host, cookies := range cookiesByHost {
|
||||
url, err := url.Parse(host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -67,9 +62,10 @@ func NewCookieJar(gs GetterSetter) (*Jar, error) {
|
||||
}
|
||||
|
||||
return &Jar{
|
||||
jar: jar,
|
||||
pantry: pantry,
|
||||
locker: &sync.Mutex{},
|
||||
jar: jar,
|
||||
settings: s,
|
||||
cookies: cookiesByHost,
|
||||
locker: &sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -79,9 +75,13 @@ func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||
|
||||
j.jar.SetCookies(u, cookies)
|
||||
|
||||
if err := j.pantry.persistCookies(u.Scheme+"://"+u.Host, cookies); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to persist cookie")
|
||||
for _, cookie := range cookies {
|
||||
if cookie.MaxAge > 0 {
|
||||
cookie.Expires = time.Now().Add(time.Duration(cookie.MaxAge) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
j.cookies[fmt.Sprintf("%v://%v", u.Scheme, u.Host)] = cookies
|
||||
}
|
||||
|
||||
func (j *Jar) Cookies(u *url.URL) []*http.Cookie {
|
||||
@ -90,3 +90,54 @@ func (j *Jar) Cookies(u *url.URL) []*http.Cookie {
|
||||
|
||||
return j.jar.Cookies(u)
|
||||
}
|
||||
|
||||
// PersistCookies persists the cookies to disk.
|
||||
func (j *Jar) PersistCookies() error {
|
||||
j.locker.Lock()
|
||||
defer j.locker.Unlock()
|
||||
|
||||
rawCookies, err := json.Marshal(j.cookies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
j.settings.Set(settings.CookiesKey, string(rawCookies))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadCookies loads all non-expired cookies from disk.
|
||||
func loadCookies(s *settings.Settings) (cookiesByHost, error) {
|
||||
rawCookies := s.Get(settings.CookiesKey)
|
||||
|
||||
if rawCookies == "" {
|
||||
return make(cookiesByHost), nil
|
||||
}
|
||||
|
||||
var cookiesByHost cookiesByHost
|
||||
|
||||
if err := json.Unmarshal([]byte(rawCookies), &cookiesByHost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for host, cookies := range cookiesByHost {
|
||||
if validCookies := discardExpiredCookies(cookies); len(validCookies) > 0 {
|
||||
cookiesByHost[host] = validCookies
|
||||
}
|
||||
}
|
||||
|
||||
return cookiesByHost, nil
|
||||
}
|
||||
|
||||
// discardExpiredCookies returns all the given cookies which aren't expired.
|
||||
func discardExpiredCookies(cookies []*http.Cookie) []*http.Cookie {
|
||||
var validCookies []*http.Cookie
|
||||
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Expires.After(time.Now()) {
|
||||
validCookies = append(validCookies, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
return validCookies
|
||||
}
|
||||
|
||||
@ -18,11 +18,13 @@
|
||||
package cookies
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -35,7 +37,7 @@ func TestJarGetSet(t *testing.T) {
|
||||
})
|
||||
defer ts.Close()
|
||||
|
||||
client := getClientWithJar(t, make(testGetterSetter))
|
||||
client, _ := getClientWithJar(t, newFakeSettings())
|
||||
|
||||
// Hit a server that sets some cookies.
|
||||
setRes, err := client.Get(ts.URL + "/set")
|
||||
@ -61,10 +63,10 @@ func TestJarLoad(t *testing.T) {
|
||||
defer ts.Close()
|
||||
|
||||
// This will be our "persistent storage" from which the cookie jar should load cookies.
|
||||
gs := make(testGetterSetter)
|
||||
s := newFakeSettings()
|
||||
|
||||
// This client saves cookies to persistent storage.
|
||||
oldClient := getClientWithJar(t, gs)
|
||||
oldClient, jar := getClientWithJar(t, s)
|
||||
|
||||
// Hit a server that sets some cookies.
|
||||
setRes, err := oldClient.Get(ts.URL + "/set")
|
||||
@ -73,8 +75,11 @@ func TestJarLoad(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, setRes.Body.Close())
|
||||
|
||||
// Save the cookies.
|
||||
require.NoError(t, jar.PersistCookies())
|
||||
|
||||
// This client loads cookies from persistent storage.
|
||||
newClient := getClientWithJar(t, gs)
|
||||
newClient, _ := getClientWithJar(t, s)
|
||||
|
||||
// Hit a server that checks the cookies are there.
|
||||
getRes, err := newClient.Get(ts.URL + "/get")
|
||||
@ -93,10 +98,10 @@ func TestJarExpiry(t *testing.T) {
|
||||
defer ts.Close()
|
||||
|
||||
// This will be our "persistent storage" from which the cookie jar should load cookies.
|
||||
gs := make(testGetterSetter)
|
||||
s := newFakeSettings()
|
||||
|
||||
// This client saves cookies to persistent storage.
|
||||
oldClient := getClientWithJar(t, gs)
|
||||
oldClient, jar1 := getClientWithJar(t, s)
|
||||
|
||||
// Hit a server that sets some cookies.
|
||||
setRes, err := oldClient.Get(ts.URL + "/set")
|
||||
@ -105,15 +110,21 @@ func TestJarExpiry(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, setRes.Body.Close())
|
||||
|
||||
// Save the cookies.
|
||||
require.NoError(t, jar1.PersistCookies())
|
||||
|
||||
// Wait until the second cookie expires.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Load a client, which will clear out expired cookies.
|
||||
_ = getClientWithJar(t, gs)
|
||||
_, jar2 := getClientWithJar(t, s)
|
||||
|
||||
assert.Contains(t, gs["cookies"], "TestName1")
|
||||
assert.NotContains(t, gs["cookies"], "TestName2")
|
||||
assert.Contains(t, gs["cookies"], "TestName3")
|
||||
// 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")
|
||||
}
|
||||
|
||||
type testCookie struct {
|
||||
@ -121,11 +132,11 @@ type testCookie struct {
|
||||
maxAge int
|
||||
}
|
||||
|
||||
func getClientWithJar(t *testing.T, gs GetterSetter) *http.Client {
|
||||
jar, err := NewCookieJar(gs)
|
||||
func getClientWithJar(t *testing.T, s *settings.Settings) (*http.Client, *Jar) {
|
||||
jar, err := NewCookieJar(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &http.Client{Jar: jar}
|
||||
return &http.Client{Jar: jar}, jar
|
||||
}
|
||||
|
||||
func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server {
|
||||
@ -157,12 +168,12 @@ func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server {
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
type testGetterSetter map[string]string
|
||||
// newFakeSettings creates a temporary folder for files.
|
||||
func newFakeSettings() *settings.Settings {
|
||||
dir, err := ioutil.TempDir("", "test-settings")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
func (p testGetterSetter) Set(key, value string) {
|
||||
p[key] = value
|
||||
}
|
||||
|
||||
func (p testGetterSetter) Get(key string) string {
|
||||
return p[key]
|
||||
return settings.New(dir)
|
||||
}
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
// 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 cookies
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
)
|
||||
|
||||
// pantry persists and loads cookies to some persistent storage location.
|
||||
type pantry struct {
|
||||
gs GetterSetter
|
||||
}
|
||||
|
||||
func (p *pantry) persistCookies(host string, cookies []*http.Cookie) error {
|
||||
for _, cookie := range cookies {
|
||||
if cookie.MaxAge > 0 {
|
||||
cookie.Expires = time.Now().Add(time.Duration(cookie.MaxAge) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
cookiesByHost, err := p.loadFromJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cookiesByHost[host] = cookies
|
||||
|
||||
return p.saveToJSON(cookiesByHost)
|
||||
}
|
||||
|
||||
func (p *pantry) discardExpiredCookies() error {
|
||||
cookiesByHost, err := p.loadFromJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for host, cookies := range cookiesByHost {
|
||||
cookiesByHost[host] = discardExpiredCookies(cookies)
|
||||
}
|
||||
|
||||
return p.saveToJSON(cookiesByHost)
|
||||
}
|
||||
|
||||
type cookiesByHost map[string][]*http.Cookie
|
||||
|
||||
func (p *pantry) loadFromJSON() (cookiesByHost, error) {
|
||||
b := p.gs.Get(preferences.CookiesKey)
|
||||
|
||||
if b == "" {
|
||||
return make(cookiesByHost), nil
|
||||
}
|
||||
|
||||
var cookies cookiesByHost
|
||||
|
||||
if err := json.Unmarshal([]byte(b), &cookies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
func (p *pantry) saveToJSON(cookies cookiesByHost) error {
|
||||
b, err := json.Marshal(cookies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.gs.Set(preferences.CookiesKey, string(b))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func discardExpiredCookies(cookies []*http.Cookie) (validCookies []*http.Cookie) {
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Expires.After(time.Now()) {
|
||||
validCookies = append(validCookies, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
42
internal/crash/actions.go
Normal file
42
internal/crash/actions.go
Normal file
@ -0,0 +1,42 @@
|
||||
// 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 crash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/0xAX/notificator"
|
||||
)
|
||||
|
||||
// ShowErrorNotification shows a system notification that the app with the given appName has crashed.
|
||||
// NOTE: Icons shouldn't be hardcoded.
|
||||
func ShowErrorNotification(appName string) RecoveryAction {
|
||||
return func(r interface{}) error {
|
||||
notify := notificator.New(notificator.Options{
|
||||
DefaultIcon: "../frontend/ui/icon/icon.png",
|
||||
AppName: appName,
|
||||
})
|
||||
|
||||
return notify.Push(
|
||||
"Fatal Error",
|
||||
fmt.Sprintf("%v has encountered a fatal error.", appName),
|
||||
"/frontend/icon/icon.png",
|
||||
notificator.UR_CRITICAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
54
internal/crash/handler.go
Normal file
54
internal/crash/handler.go
Normal file
@ -0,0 +1,54 @@
|
||||
// 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 crash implements a crash handler with configurable recovery actions.
|
||||
package crash
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type RecoveryAction func(interface{}) error
|
||||
|
||||
type Handler struct {
|
||||
actions []RecoveryAction
|
||||
}
|
||||
|
||||
func NewHandler(actions ...RecoveryAction) *Handler {
|
||||
return &Handler{actions: actions}
|
||||
}
|
||||
|
||||
func (h *Handler) AddRecoveryAction(action RecoveryAction) *Handler {
|
||||
h.actions = append(h.actions, action)
|
||||
return h
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,36 +15,44 @@
|
||||
// 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 updates
|
||||
package crash
|
||||
|
||||
const (
|
||||
InfoCurrentVersion = 1 + iota
|
||||
InfoDownloading
|
||||
InfoVerifying
|
||||
InfoUnpacking
|
||||
InfoUpgrading
|
||||
InfoQuitApp
|
||||
InfoRestartApp
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Progress struct {
|
||||
Processed float32 // fraction of finished procedure [0.0-1.0]
|
||||
Description int // description by code (needs to be translated anyway)
|
||||
Err error // occurred error
|
||||
channel chan<- Progress
|
||||
}
|
||||
func TestHandler(t *testing.T) {
|
||||
var s string
|
||||
|
||||
func (s *Progress) Update() {
|
||||
s.channel <- *s
|
||||
}
|
||||
h := NewHandler(
|
||||
func(r interface{}) error {
|
||||
s += fmt.Sprintf("1: %v\n", r)
|
||||
return nil
|
||||
},
|
||||
func(r interface{}) error {
|
||||
s += fmt.Sprintf("2: %v\n", r)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
func (s *Progress) UpdateDescription(description int) {
|
||||
s.Description = description
|
||||
s.Processed = 0
|
||||
s.Update()
|
||||
}
|
||||
h.
|
||||
AddRecoveryAction(func(r interface{}) error {
|
||||
s += fmt.Sprintf("3: %v\n", r)
|
||||
return nil
|
||||
}).
|
||||
AddRecoveryAction(func(r interface{}) error {
|
||||
s += fmt.Sprintf("4: %v\n", r)
|
||||
return nil
|
||||
})
|
||||
|
||||
func (s *Progress) UpdateProcessed(processed float32) {
|
||||
s.Processed = processed
|
||||
s.Update()
|
||||
defer func() {
|
||||
assert.Equal(t, "1: thing\n2: thing\n3: thing\n4: thing\n", s)
|
||||
}()
|
||||
|
||||
defer h.HandlePanic()
|
||||
|
||||
panic("thing")
|
||||
}
|
||||
@ -27,6 +27,7 @@ import (
|
||||
// Constants of events used by the event listener in bridge.
|
||||
const (
|
||||
ErrorEvent = "error"
|
||||
CredentialsErrorEvent = "credentialsError"
|
||||
CloseConnectionEvent = "closeConnection"
|
||||
LogoutEvent = "logout"
|
||||
AddressChangedEvent = "addressChanged"
|
||||
@ -50,4 +51,5 @@ func SetupEvents(listener listener.Listener) {
|
||||
listener.SetLimit(LogoutEvent, LogoutEventTimeout)
|
||||
listener.SetBuffer(TLSCertIssue)
|
||||
listener.SetBuffer(ErrorEvent)
|
||||
listener.SetBuffer(CredentialsErrorEvent)
|
||||
}
|
||||
|
||||
@ -21,7 +21,8 @@ package cliie
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
@ -35,31 +36,33 @@ var (
|
||||
type frontendCLI struct {
|
||||
*ishell.Shell
|
||||
|
||||
config *config.Config
|
||||
locations *locations.Locations
|
||||
eventListener listener.Listener
|
||||
updates types.Updater
|
||||
updater types.Updater
|
||||
ie types.ImportExporter
|
||||
|
||||
appRestart bool
|
||||
restarter types.Restarter
|
||||
}
|
||||
|
||||
// New returns a new CLI frontend configured with the given options.
|
||||
func New( //nolint[funlen]
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
|
||||
locations *locations.Locations,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
ie types.ImportExporter,
|
||||
restarter types.Restarter,
|
||||
) *frontendCLI { //nolint[golint]
|
||||
fe := &frontendCLI{
|
||||
Shell: ishell.New(),
|
||||
|
||||
config: config,
|
||||
locations: locations,
|
||||
eventListener: eventListener,
|
||||
updates: updates,
|
||||
updater: updater,
|
||||
ie: ie,
|
||||
|
||||
appRestart: false,
|
||||
restarter: restarter,
|
||||
}
|
||||
|
||||
// Clear commands.
|
||||
@ -99,11 +102,6 @@ func New( //nolint[funlen]
|
||||
Aliases: []string{"man"},
|
||||
Func: fe.printManual,
|
||||
})
|
||||
fe.AddCmd(&ishell.Cmd{Name: "release-notes",
|
||||
Help: "print release notes. (aliases: notes, fixed-bugs, bugs, ver, version)",
|
||||
Aliases: []string{"notes", "fixed-bugs", "bugs", "ver", "version"},
|
||||
Func: fe.printLocalReleaseNotes,
|
||||
})
|
||||
fe.AddCmd(&ishell.Cmd{Name: "credits",
|
||||
Help: "print used resources.",
|
||||
Func: fe.printCredits,
|
||||
@ -175,13 +173,12 @@ func New( //nolint[funlen]
|
||||
defer panicHandler.HandlePanic()
|
||||
fe.watchEvents()
|
||||
}()
|
||||
fe.eventListener.RetryEmit(events.TLSCertIssue)
|
||||
fe.eventListener.RetryEmit(events.ErrorEvent)
|
||||
return fe
|
||||
}
|
||||
|
||||
func (f *frontendCLI) watchEvents() {
|
||||
errorCh := f.getEventChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
|
||||
internetOffCh := f.getEventChannel(events.InternetOffEvent)
|
||||
internetOnCh := f.getEventChannel(events.InternetOnEvent)
|
||||
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent)
|
||||
@ -191,6 +188,8 @@ func (f *frontendCLI) watchEvents() {
|
||||
select {
|
||||
case errorDetails := <-errorCh:
|
||||
f.Println("Import-Export failed:", errorDetails)
|
||||
case <-credentialsErrorCh:
|
||||
f.notifyCredentialsError()
|
||||
case <-internetOffCh:
|
||||
f.notifyInternetOff()
|
||||
case <-internetOnCh:
|
||||
@ -212,21 +211,12 @@ func (f *frontendCLI) watchEvents() {
|
||||
func (f *frontendCLI) getEventChannel(event string) <-chan string {
|
||||
ch := make(chan string)
|
||||
f.eventListener.Add(event, ch)
|
||||
f.eventListener.RetryEmit(event)
|
||||
return ch
|
||||
}
|
||||
|
||||
// IsAppRestarting returns whether the app is currently set to restart.
|
||||
func (f *frontendCLI) IsAppRestarting() bool {
|
||||
return f.appRestart
|
||||
}
|
||||
|
||||
// Loop starts the frontend loop with an interactive shell.
|
||||
func (f *frontendCLI) Loop(credentialsError error) error {
|
||||
if credentialsError != nil {
|
||||
f.notifyCredentialsError()
|
||||
return credentialsError
|
||||
}
|
||||
|
||||
func (f *frontendCLI) Loop() error {
|
||||
f.Print(`
|
||||
Welcome to ProtonMail Import-Export app interactive shell
|
||||
|
||||
@ -235,3 +225,11 @@ WARNING: The CLI is an experimental feature and does not yet cover all functiona
|
||||
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) SetVersion(version updater.VersionInfo) {}
|
||||
func (f *frontendCLI) NotifySilentUpdateInstalled() {}
|
||||
func (f *frontendCLI) NotifySilentUpdateError(err error) {}
|
||||
|
||||
@ -24,7 +24,7 @@ import (
|
||||
func (f *frontendCLI) restart(c *ishell.Context) {
|
||||
if f.yesNoQuestion("Are you sure you want to restart the Import-Export app") {
|
||||
f.Println("Restarting the Import-Export app...")
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,11 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printLogDir(c *ishell.Context) {
|
||||
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
|
||||
if path, err := f.locations.ProvideLogsPath(); err != nil {
|
||||
f.Println("Failed to determine location of log files")
|
||||
} else {
|
||||
f.Println("Log files are stored in\n\n ", path)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printManual(c *ishell.Context) {
|
||||
|
||||
@ -21,41 +21,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
|
||||
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
|
||||
if err != nil {
|
||||
f.printAndLogError("Cannot retrieve version info: ", err)
|
||||
f.checkInternetConnection(c)
|
||||
return
|
||||
}
|
||||
if isUpToDate {
|
||||
f.Println("Your version is up to date.")
|
||||
} else {
|
||||
f.notifyNeedUpgrade()
|
||||
f.Println("")
|
||||
f.printReleaseNotes(latestVersionInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) {
|
||||
localVersion := f.updates.GetLocalVersion()
|
||||
f.printReleaseNotes(localVersion)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
|
||||
f.Println(bold("ProtonMail Import-Export app "+versionInfo.Version), "\n")
|
||||
if versionInfo.ReleaseNotes != "" {
|
||||
f.Println(bold("Release Notes"))
|
||||
f.Println(versionInfo.ReleaseNotes)
|
||||
}
|
||||
if versionInfo.ReleaseFixedBugs != "" {
|
||||
f.Println(bold("Fixed bugs"))
|
||||
f.Println(versionInfo.ReleaseFixedBugs)
|
||||
}
|
||||
f.Println("Your version is up to date.")
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printCredits(c *ishell.Context) {
|
||||
|
||||
@ -93,10 +93,15 @@ func (f *frontendCLI) notifyLogout(address string) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyNeedUpgrade() {
|
||||
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
|
||||
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)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyCredentialsError() {
|
||||
func (f *frontendCLI) notifyCredentialsError() { // nolint[unused]
|
||||
// Print in 80-column width.
|
||||
f.Println("ProtonMail Import-Export app is not able to detect a supported password manager")
|
||||
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")
|
||||
|
||||
@ -21,8 +21,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
@ -65,13 +65,13 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
|
||||
|
||||
func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) {
|
||||
smtpSecurity := "STARTTLS"
|
||||
if f.preferences.GetBool(preferences.SMTPSSLKey) {
|
||||
if f.settings.GetBool(settings.SMTPSSLKey) {
|
||||
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.preferences.GetInt(preferences.IMAPPortKey),
|
||||
f.settings.GetInt(settings.IMAPPortKey),
|
||||
address,
|
||||
user.GetBridgePassword(),
|
||||
"STARTTLS",
|
||||
@ -79,7 +79,7 @@ func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) {
|
||||
f.Println("")
|
||||
f.Printf("SMTP Settings\nAddress: %s\nSMTP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
|
||||
bridge.Host,
|
||||
f.preferences.GetInt(preferences.SMTPPortKey),
|
||||
f.settings.GetInt(settings.SMTPPortKey),
|
||||
address,
|
||||
user.GetBridgePassword(),
|
||||
smtpSecurity,
|
||||
|
||||
@ -19,9 +19,11 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
@ -35,34 +37,36 @@ var (
|
||||
type frontendCLI struct {
|
||||
*ishell.Shell
|
||||
|
||||
config *config.Config
|
||||
preferences *config.Preferences
|
||||
locations *locations.Locations
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
updates types.Updater
|
||||
updater types.Updater
|
||||
bridge types.Bridger
|
||||
|
||||
appRestart bool
|
||||
restarter types.Restarter
|
||||
}
|
||||
|
||||
// New returns a new CLI frontend configured with the given options.
|
||||
func New( //nolint[funlen]
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
preferences *config.Preferences,
|
||||
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
bridge types.Bridger,
|
||||
restarter types.Restarter,
|
||||
) *frontendCLI { //nolint[golint]
|
||||
fe := &frontendCLI{
|
||||
Shell: ishell.New(),
|
||||
|
||||
config: config,
|
||||
preferences: preferences,
|
||||
locations: locations,
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
updates: updates,
|
||||
updater: updater,
|
||||
bridge: bridge,
|
||||
|
||||
appRestart: false,
|
||||
restarter: restarter,
|
||||
}
|
||||
|
||||
// Clear commands.
|
||||
@ -134,11 +138,7 @@ func New( //nolint[funlen]
|
||||
Aliases: []string{"man"},
|
||||
Func: fe.printManual,
|
||||
})
|
||||
fe.AddCmd(&ishell.Cmd{Name: "release-notes",
|
||||
Help: "print release notes. (aliases: notes, fixed-bugs, bugs, ver, version)",
|
||||
Aliases: []string{"notes", "fixed-bugs", "bugs", "ver", "version"},
|
||||
Func: fe.printLocalReleaseNotes,
|
||||
})
|
||||
|
||||
fe.AddCmd(&ishell.Cmd{Name: "credits",
|
||||
Help: "print used resources.",
|
||||
Func: fe.printCredits,
|
||||
@ -185,13 +185,12 @@ func New( //nolint[funlen]
|
||||
defer panicHandler.HandlePanic()
|
||||
fe.watchEvents()
|
||||
}()
|
||||
fe.eventListener.RetryEmit(events.TLSCertIssue)
|
||||
fe.eventListener.RetryEmit(events.ErrorEvent)
|
||||
return fe
|
||||
}
|
||||
|
||||
func (f *frontendCLI) watchEvents() {
|
||||
errorCh := f.getEventChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
|
||||
internetOffCh := f.getEventChannel(events.InternetOffEvent)
|
||||
internetOnCh := f.getEventChannel(events.InternetOnEvent)
|
||||
addressChangedCh := f.getEventChannel(events.AddressChangedEvent)
|
||||
@ -202,6 +201,8 @@ func (f *frontendCLI) watchEvents() {
|
||||
select {
|
||||
case errorDetails := <-errorCh:
|
||||
f.Println("Bridge failed:", errorDetails)
|
||||
case <-credentialsErrorCh:
|
||||
f.notifyCredentialsError()
|
||||
case <-internetOffCh:
|
||||
f.notifyInternetOff()
|
||||
case <-internetOnCh:
|
||||
@ -225,21 +226,12 @@ func (f *frontendCLI) watchEvents() {
|
||||
func (f *frontendCLI) getEventChannel(event string) <-chan string {
|
||||
ch := make(chan string)
|
||||
f.eventListener.Add(event, ch)
|
||||
f.eventListener.RetryEmit(event)
|
||||
return ch
|
||||
}
|
||||
|
||||
// IsAppRestarting returns whether the app is currently set to restart.
|
||||
func (f *frontendCLI) IsAppRestarting() bool {
|
||||
return f.appRestart
|
||||
}
|
||||
|
||||
// Loop starts the frontend loop with an interactive shell.
|
||||
func (f *frontendCLI) Loop(credentialsError error) error {
|
||||
if credentialsError != nil {
|
||||
f.notifyCredentialsError()
|
||||
return credentialsError
|
||||
}
|
||||
|
||||
func (f *frontendCLI) Loop() error {
|
||||
f.Print(`
|
||||
Welcome to ProtonMail Bridge interactive shell
|
||||
___....___
|
||||
@ -260,3 +252,11 @@ func (f *frontendCLI) Loop(credentialsError error) 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) SetVersion(version updater.VersionInfo) {}
|
||||
func (f *frontendCLI) NotifySilentUpdateInstalled() {}
|
||||
func (f *frontendCLI) NotifySilentUpdateError(err error) {}
|
||||
|
||||
@ -22,7 +22,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
@ -34,7 +34,7 @@ var (
|
||||
func (f *frontendCLI) restart(c *ishell.Context) {
|
||||
if f.yesNoQuestion("Are you sure you want to restart the Bridge") {
|
||||
f.Println("Restarting Bridge...")
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
@ -48,7 +48,11 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printLogDir(c *ishell.Context) {
|
||||
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
|
||||
if path, err := f.locations.ProvideLogsPath(); err != nil {
|
||||
f.Println("Failed to determine location of log files")
|
||||
} else {
|
||||
f.Println("Log files are stored in\n\n ", path)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printManual(c *ishell.Context) {
|
||||
@ -69,7 +73,7 @@ func (f *frontendCLI) deleteCache(c *ishell.Context) {
|
||||
f.Println("Cached cleared, restarting bridge")
|
||||
// Clearing data removes everything (db, preferences, ...)
|
||||
// so everything has to be stopped and started again.
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
|
||||
@ -77,7 +81,7 @@ func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
isSSL := f.preferences.GetBool(preferences.SMTPSSLKey)
|
||||
isSSL := f.settings.GetBool(settings.SMTPSSLKey)
|
||||
newSecurity := "SSL"
|
||||
if isSSL {
|
||||
newSecurity = "STARTTLS"
|
||||
@ -86,9 +90,9 @@ func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
|
||||
msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q and restart the Bridge", newSecurity)
|
||||
|
||||
if f.yesNoQuestion(msg) {
|
||||
f.preferences.SetBool(preferences.SMTPSSLKey, !isSSL)
|
||||
f.settings.SetBool(settings.SMTPSSLKey, !isSSL)
|
||||
f.Println("Restarting Bridge...")
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
@ -97,14 +101,14 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
currentPort = f.preferences.Get(preferences.IMAPPortKey)
|
||||
currentPort = f.settings.Get(settings.IMAPPortKey)
|
||||
newIMAPPort := f.readStringInAttempts("Set IMAP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
|
||||
if newIMAPPort == "" {
|
||||
newIMAPPort = currentPort
|
||||
}
|
||||
imapPortChanged := newIMAPPort != currentPort
|
||||
|
||||
currentPort = f.preferences.Get(preferences.SMTPPortKey)
|
||||
currentPort = f.settings.Get(settings.SMTPPortKey)
|
||||
newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
|
||||
if newSMTPPort == "" {
|
||||
newSMTPPort = currentPort
|
||||
@ -118,10 +122,10 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
|
||||
|
||||
if imapPortChanged || smtpPortChanged {
|
||||
f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort)
|
||||
f.preferences.Set(preferences.IMAPPortKey, newIMAPPort)
|
||||
f.preferences.Set(preferences.SMTPPortKey, newSMTPPort)
|
||||
f.settings.Set(settings.IMAPPortKey, newIMAPPort)
|
||||
f.settings.Set(settings.SMTPPortKey, newSMTPPort)
|
||||
f.Println("Restarting Bridge...")
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
} else {
|
||||
f.Println("Nothing changed")
|
||||
@ -129,16 +133,16 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
|
||||
if f.preferences.GetBool(preferences.AllowProxyKey) {
|
||||
if f.settings.GetBool(settings.AllowProxyKey) {
|
||||
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.preferences.SetBool(preferences.AllowProxyKey, false)
|
||||
f.settings.SetBool(settings.AllowProxyKey, false)
|
||||
f.bridge.DisallowProxy()
|
||||
}
|
||||
} else {
|
||||
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.preferences.SetBool(preferences.AllowProxyKey, true)
|
||||
f.settings.SetBool(settings.AllowProxyKey, true)
|
||||
f.bridge.AllowProxy()
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,41 +21,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
|
||||
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
|
||||
if err != nil {
|
||||
f.printAndLogError("Cannot retrieve version info: ", err)
|
||||
f.checkInternetConnection(c)
|
||||
return
|
||||
}
|
||||
if isUpToDate {
|
||||
f.Println("Your version is up to date.")
|
||||
} else {
|
||||
f.notifyNeedUpgrade()
|
||||
f.Println("")
|
||||
f.printReleaseNotes(latestVersionInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) {
|
||||
localVersion := f.updates.GetLocalVersion()
|
||||
f.printReleaseNotes(localVersion)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
|
||||
f.Println(bold("ProtonMail Bridge "+versionInfo.Version), "\n")
|
||||
if versionInfo.ReleaseNotes != "" {
|
||||
f.Println(bold("Release Notes"))
|
||||
f.Println(versionInfo.ReleaseNotes)
|
||||
}
|
||||
if versionInfo.ReleaseFixedBugs != "" {
|
||||
f.Println(bold("Fixed bugs"))
|
||||
f.Println(versionInfo.ReleaseFixedBugs)
|
||||
}
|
||||
f.Println("Your version is up to date.")
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printCredits(c *ishell.Context) {
|
||||
|
||||
@ -93,10 +93,15 @@ func (f *frontendCLI) notifyLogout(address string) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyNeedUpgrade() {
|
||||
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
|
||||
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)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyCredentialsError() {
|
||||
func (f *frontendCLI) notifyCredentialsError() { // nolint[unused]
|
||||
// Print in 80-column width.
|
||||
f.Println("ProtonMail Bridge is not able to detect a supported password manager")
|
||||
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")
|
||||
|
||||
@ -19,15 +19,17 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"github.com/0xAX/notificator"
|
||||
"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/frontend/cli"
|
||||
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
|
||||
qtie "github.com/ProtonMail/proton-bridge/internal/frontend/qt-ie"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -38,55 +40,92 @@ var (
|
||||
|
||||
// Frontend is an interface to be implemented by each frontend type (cli, gui, html).
|
||||
type Frontend interface {
|
||||
Loop(credentialsError error) error
|
||||
IsAppRestarting() bool
|
||||
}
|
||||
|
||||
// HandlePanic handles panics which occur for users with GUI.
|
||||
func HandlePanic(appName string) {
|
||||
notify := notificator.New(notificator.Options{
|
||||
DefaultIcon: "../frontend/ui/icon/icon.png",
|
||||
AppName: appName,
|
||||
})
|
||||
_ = notify.Push("Fatal Error", "The "+appName+" has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
|
||||
Loop() error
|
||||
NotifyManualUpdate(update updater.VersionInfo, canInstall bool)
|
||||
SetVersion(update updater.VersionInfo)
|
||||
NotifySilentUpdateInstalled()
|
||||
NotifySilentUpdateError(error)
|
||||
}
|
||||
|
||||
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
|
||||
func New(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType string,
|
||||
showWindowOnStart bool,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
preferences *config.Preferences,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
bridge *bridge.Bridge,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
restarter types.Restarter,
|
||||
) Frontend {
|
||||
bridgeWrap := types.NewBridgeWrap(bridge)
|
||||
return new(version, buildVersion, frontendType, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridgeWrap, noEncConfirmator)
|
||||
return newBridgeFrontend(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType,
|
||||
showWindowOnStart,
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
bridgeWrap,
|
||||
noEncConfirmator,
|
||||
autostart,
|
||||
restarter,
|
||||
)
|
||||
}
|
||||
|
||||
func new(
|
||||
func newBridgeFrontend(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType string,
|
||||
showWindowOnStart bool,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
preferences *config.Preferences,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
restarter types.Restarter,
|
||||
) Frontend {
|
||||
switch frontendType {
|
||||
case "cli":
|
||||
return cli.New(panicHandler, config, preferences, eventListener, updates, bridge)
|
||||
return cli.New(
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
bridge,
|
||||
restarter,
|
||||
)
|
||||
default:
|
||||
return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
|
||||
return qt.New(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
showWindowOnStart,
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
bridge,
|
||||
noEncConfirmator,
|
||||
autostart,
|
||||
restarter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,31 +133,67 @@ func new(
|
||||
func NewImportExport(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType string,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
ie *importexport.ImportExport,
|
||||
restarter types.Restarter,
|
||||
) Frontend {
|
||||
ieWrap := types.NewImportExportWrap(ie)
|
||||
return newImportExport(version, buildVersion, frontendType, panicHandler, config, eventListener, updates, ieWrap)
|
||||
return newIEFrontend(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType,
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
ieWrap,
|
||||
restarter,
|
||||
)
|
||||
}
|
||||
|
||||
func newImportExport(
|
||||
func newIEFrontend(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType string,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
ie types.ImportExporter,
|
||||
restarter types.Restarter,
|
||||
) Frontend {
|
||||
switch frontendType {
|
||||
case "cli":
|
||||
return cliie.New(panicHandler, config, eventListener, updates, ie)
|
||||
return cliie.New(
|
||||
panicHandler,
|
||||
locations,
|
||||
eventListener,
|
||||
updater,
|
||||
ie,
|
||||
restarter,
|
||||
)
|
||||
default:
|
||||
return qtie.New(version, buildVersion, panicHandler, config, eventListener, updates, ie)
|
||||
return qtie.New(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
ie,
|
||||
restarter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ Dialog {
|
||||
target: timer
|
||||
onTriggered: {
|
||||
go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked)
|
||||
go.isRestarting = true
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +137,7 @@ Dialog {
|
||||
spacing: Style.dialog.spacing
|
||||
ButtonRounded {
|
||||
id:buttonNo
|
||||
visible: root.state != "toggleEarlyAccess"
|
||||
color_main: Style.dialog.text
|
||||
fa_icon: Style.fa.times
|
||||
text: qsTr("No")
|
||||
@ -148,7 +149,7 @@ Dialog {
|
||||
color_minor: Style.main.textBlue
|
||||
isOpaque: true
|
||||
fa_icon: Style.fa.check
|
||||
text: qsTr("Yes")
|
||||
text: root.state == "toggleEarlyAccess" ? qsTr("Ok") : qsTr("Yes")
|
||||
onClicked : {
|
||||
currentIndex=1
|
||||
root.confirmed()
|
||||
@ -292,6 +293,17 @@ Dialog {
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "toggleEarlyAccess"
|
||||
PropertyChanges {
|
||||
target: root
|
||||
currentIndex : 0
|
||||
question : qsTr("Do you want to be the first to get the latest updates? Please keep in mind that early versions may be less stable.")
|
||||
note : ""
|
||||
title : qsTr("Enable early access")
|
||||
answer : qsTr("Enabling early access...")
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "noKeychain"
|
||||
PropertyChanges {
|
||||
@ -340,13 +352,9 @@ Dialog {
|
||||
winMain.dialogAddUser .visible = false
|
||||
winMain.dialogChangePort .visible = false
|
||||
winMain.dialogCredits .visible = false
|
||||
winMain.dialogVersionInfo .visible = false
|
||||
// dialogFirstStart should reappear again after closing global
|
||||
root.visible = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
onConfirmed : {
|
||||
if (state == "quit" || state == "instance exists" ) {
|
||||
timer.interval = 1000
|
||||
@ -360,17 +368,18 @@ Dialog {
|
||||
Connections {
|
||||
target: timer
|
||||
onTriggered: {
|
||||
if ( state == "addressmode" ) { go.switchAddressMode (input) }
|
||||
if ( state == "clearChain" ) { go.clearKeychain () }
|
||||
if ( state == "clearCache" ) { go.clearCache () }
|
||||
if ( state == "deleteUser" ) { go.deleteAccount (input, checkBoxWrapper.isChecked) }
|
||||
if ( state == "logout" ) { go.logoutAccount (input) }
|
||||
if ( state == "toggleAutoStart" ) { go.toggleAutoStart () }
|
||||
if ( state == "toggleAllowProxy" ) { go.toggleAllowProxy () }
|
||||
if ( state == "quit" ) { Qt.quit () }
|
||||
if ( state == "instance exists" ) { Qt.quit () }
|
||||
if ( state == "noKeychain" ) { Qt.quit () }
|
||||
if ( state == "checkUpdates" ) { go.runCheckVersion (true) }
|
||||
if ( state == "addressmode" ) { go.switchAddressMode (input) }
|
||||
if ( state == "clearChain" ) { go.clearKeychain () }
|
||||
if ( state == "clearCache" ) { go.clearCache () }
|
||||
if ( state == "deleteUser" ) { go.deleteAccount (input, checkBoxWrapper.isChecked) }
|
||||
if ( state == "logout" ) { go.logoutAccount (input) }
|
||||
if ( state == "toggleAutoStart" ) { go.toggleAutoStart () }
|
||||
if ( state == "toggleAllowProxy" ) { go.toggleAllowProxy () }
|
||||
if ( state == "toggleEarlyAccess" ) { go.toggleEarlyAccess () }
|
||||
if ( state == "quit" ) { Qt.quit () }
|
||||
if ( state == "instance exists" ) { Qt.quit () }
|
||||
if ( state == "noKeychain" ) { Qt.quit () }
|
||||
if ( state == "checkUpdates" ) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -74,9 +74,7 @@ Item {
|
||||
rightIcon.text : Style.fa.chevron_circle_right
|
||||
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
|
||||
onClicked: {
|
||||
dialogGlobal.state="checkUpdates"
|
||||
dialogGlobal.show()
|
||||
dialogGlobal.confirmed()
|
||||
go.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,10 +135,7 @@ Item {
|
||||
textColor : Style.main.textDisabled
|
||||
fontSize : Style.main.fontSize
|
||||
textUnderline : true
|
||||
onClicked : {
|
||||
go.getLocalVersionInfo()
|
||||
winMain.dialogVersionInfo.show()
|
||||
}
|
||||
onClicked : gui.openReleaseNotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,6 @@ Window {
|
||||
property alias dialogUpdate : dialogUpdate
|
||||
property alias dialogFirstStart : dialogFirstStart
|
||||
property alias dialogGlobal : dialogGlobal
|
||||
property alias dialogVersionInfo : dialogVersionInfo
|
||||
property alias dialogConnectionTroubleshoot : dialogConnectionTroubleshoot
|
||||
property alias bubbleNote : bubbleNote
|
||||
property alias addAccountTip : addAccountTip
|
||||
@ -66,7 +65,6 @@ Window {
|
||||
!dialogUpdate .visible &&
|
||||
!dialogFirstStart .visible &&
|
||||
!dialogGlobal .visible &&
|
||||
!dialogVersionInfo .visible &&
|
||||
!bubbleNote .visible
|
||||
|
||||
Accessible.role: Accessible.Grouping
|
||||
@ -316,50 +314,7 @@ Window {
|
||||
|
||||
DialogUpdate {
|
||||
id: dialogUpdate
|
||||
|
||||
property string manualLinks : {
|
||||
var out = ""
|
||||
var links = go.downloadLink.split("\n")
|
||||
var l;
|
||||
for (l in links) {
|
||||
out += '<a href="%1">%1</a><br>'.arg(links[l])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
title: root.isOutdateVersion ?
|
||||
qsTr("%1 is outdated", "title of outdate dialog").arg(go.programTitle):
|
||||
qsTr("%1 update to %2", "title of update dialog").arg(go.programTitle).arg(go.newversion)
|
||||
introductionText: {
|
||||
if (root.isOutdateVersion) {
|
||||
if (go.goos=="linux") {
|
||||
return qsTr('You are using an outdated version of our software.<br>
|
||||
Please download and install the latest version to continue using %1.<br><br>
|
||||
%2',
|
||||
"Message for force-update in Linux").arg(go.programTitle).arg(dialogUpdate.manualLinks)
|
||||
} else {
|
||||
return qsTr('You are using an outdated version of our software.<br>
|
||||
Please download and install the latest version to continue using %1.<br><br>
|
||||
You can continue with the update or download and install the new version manually from<br><br>
|
||||
<a href="%2">%2</a>',
|
||||
"Message for force-update in Win/Mac").arg(go.programTitle).arg(go.landingPage)
|
||||
}
|
||||
} else {
|
||||
if (go.goos=="linux") {
|
||||
return qsTr('A new version of Bridge is available.<br>
|
||||
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
|
||||
Use your package manager to update or download and install the new version manually from<br><br>
|
||||
%3',
|
||||
"Message for update in Linux").arg("releaseNotes").arg(go.newversion).arg(dialogUpdate.manualLinks)
|
||||
} else {
|
||||
return qsTr('A new version of Bridge is available.<br>
|
||||
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
|
||||
You can continue with the update or download and install the new version manually from<br><br>
|
||||
<a href="%3">%3</a>',
|
||||
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
forceUpdate: root.isOutdateVersion
|
||||
}
|
||||
|
||||
|
||||
@ -373,25 +328,6 @@ Window {
|
||||
id: dialogTlsCert
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: dialogVersionInfo
|
||||
property bool checkVersionOnClose : false
|
||||
title: qsTr("Information about", "title of release notes page") + " v" + go.newversion
|
||||
VersionInfo { }
|
||||
onShow : {
|
||||
// Hide information bar with old version
|
||||
if (infoBar.state=="oldVersion") {
|
||||
infoBar.state="upToDate"
|
||||
dialogVersionInfo.checkVersionOnClose = true
|
||||
}
|
||||
}
|
||||
onHide : {
|
||||
// Reload current version based on online status
|
||||
if (dialogVersionInfo.checkVersionOnClose) go.runCheckVersion(false)
|
||||
dialogVersionInfo.checkVersionOnClose = false
|
||||
}
|
||||
}
|
||||
|
||||
DialogYesNo {
|
||||
id: dialogGlobal
|
||||
question : ""
|
||||
|
||||
@ -36,6 +36,24 @@ Item {
|
||||
color: Style.main.background
|
||||
}
|
||||
|
||||
// horizontall scrollbar sometimes showes up when vertical scrollbar coveres content
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
|
||||
// keeping vertical scrollbar allways visible when needed
|
||||
Connections {
|
||||
target: wrapper.ScrollBar.vertical
|
||||
onSizeChanged: {
|
||||
// ScrollBar.size == 0 at creating so no need to make it active
|
||||
if (wrapper.ScrollBar.vertical.size < 1.0 && wrapper.ScrollBar.vertical.size > 0 && !wrapper.ScrollBar.vertical.active) {
|
||||
wrapper.ScrollBar.vertical.active = true
|
||||
}
|
||||
}
|
||||
onActiveChanged: {
|
||||
wrapper.ScrollBar.vertical.active = true
|
||||
}
|
||||
}
|
||||
|
||||
// content
|
||||
Column {
|
||||
anchors.left : parent.left
|
||||
@ -97,6 +115,49 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ButtonIconText {
|
||||
id: autoUpdates
|
||||
text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates")
|
||||
leftIcon.text : Style.fa.download
|
||||
rightIcon {
|
||||
font.pointSize : Style.settings.toggleSize * Style.pt
|
||||
text : go.isAutoUpdate!=false ? Style.fa.toggle_on : Style.fa.toggle_off
|
||||
color : go.isAutoUpdate!=false ? Style.main.textBlue : Style.main.textDisabled
|
||||
}
|
||||
Accessible.description: (
|
||||
go.isAutoUpdate == false ?
|
||||
qsTr("Enable" , "Click to enable the automatic update of Bridge") :
|
||||
qsTr("Disable" , "Click to disable the automatic update of Bridge")
|
||||
) + " " + text
|
||||
onClicked: {
|
||||
go.toggleAutoUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
ButtonIconText {
|
||||
id: earlyAccess
|
||||
text: qsTr("Early access", "label for toggle that enables and disables early access")
|
||||
leftIcon.text : Style.fa.star
|
||||
rightIcon {
|
||||
font.pointSize : Style.settings.toggleSize * Style.pt
|
||||
text : go.isEarlyAccess!=false ? Style.fa.toggle_on : Style.fa.toggle_off
|
||||
color : go.isEarlyAccess!=false ? Style.main.textBlue : Style.main.textDisabled
|
||||
}
|
||||
Accessible.description: (
|
||||
go.isEarlyAccess == false ?
|
||||
qsTr("Enable" , "Click to enable early access") :
|
||||
qsTr("Disable" , "Click to disable early access")
|
||||
) + " " + text
|
||||
onClicked: {
|
||||
if (go.isEarlyAccess == true) {
|
||||
go.toggleEarlyAccess()
|
||||
} else {
|
||||
dialogGlobal.state="toggleEarlyAccess"
|
||||
dialogGlobal.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonIconText {
|
||||
id: advancedSettings
|
||||
property bool isAdvanced : !go.isDefaultPort
|
||||
@ -177,7 +238,6 @@ Item {
|
||||
dialogGlobal.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,127 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
// credits
|
||||
|
||||
import QtQuick 2.8
|
||||
import BridgeUI 1.0
|
||||
import ProtonUI 1.0
|
||||
|
||||
Item {
|
||||
Rectangle {
|
||||
id: wrapper
|
||||
anchors.centerIn: parent
|
||||
width: 2*Style.main.width/3
|
||||
height: Style.main.height - 6*Style.dialog.titleSize
|
||||
color: "transparent"
|
||||
|
||||
Flickable {
|
||||
anchors.fill : wrapper
|
||||
contentWidth : wrapper.width
|
||||
contentHeight : content.height
|
||||
flickableDirection : Flickable.VerticalFlick
|
||||
clip : true
|
||||
|
||||
|
||||
Column {
|
||||
id: content
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: wrapper.width
|
||||
spacing: Style.dialog.spacing
|
||||
|
||||
AccessibleText {
|
||||
visible: go.changelog != ""
|
||||
anchors {
|
||||
left: parent.left
|
||||
}
|
||||
font.bold: true
|
||||
font.pointSize: Style.main.fontSize * Style.pt
|
||||
color: Style.main.text
|
||||
text: qsTr("Release notes", "list of release notes for this version of the app") + ":"
|
||||
}
|
||||
|
||||
AccessibleSelectableText {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Style.main.leftMargin
|
||||
}
|
||||
font {
|
||||
pointSize : Style.main.fontSize * Style.pt
|
||||
}
|
||||
width: wrapper.width - anchors.leftMargin
|
||||
onLinkActivated: {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
wrapMode: Text.Wrap
|
||||
color: Style.main.text
|
||||
text: go.changelog
|
||||
}
|
||||
|
||||
AccessibleText {
|
||||
visible: go.bugfixes != ""
|
||||
anchors {
|
||||
left: parent.left
|
||||
}
|
||||
font.bold: true
|
||||
font.pointSize: Style.main.fontSize * Style.pt
|
||||
color: Style.main.text
|
||||
text: qsTr("Fixed bugs", "list of bugs fixed for this version of the app") + ":"
|
||||
}
|
||||
|
||||
AccessibleSelectableText {
|
||||
visible: go.bugfixes!=""
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Style.main.leftMargin
|
||||
}
|
||||
font {
|
||||
pointSize : Style.main.fontSize * Style.pt
|
||||
}
|
||||
width: wrapper.width - anchors.leftMargin
|
||||
onLinkActivated: {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
wrapMode: Text.Wrap
|
||||
color: Style.main.text
|
||||
text: go.bugfixes
|
||||
}
|
||||
|
||||
Rectangle{id:spacer; color:Style.transparent; width: Style.main.dummy; height: buttonClose.height}
|
||||
|
||||
ButtonRounded {
|
||||
id: buttonClose
|
||||
anchors.horizontalCenter: content.horizontalCenter
|
||||
text: qsTr("Close")
|
||||
onClicked: {
|
||||
dialogVersionInfo.hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AccessibleSelectableText {
|
||||
anchors.horizontalCenter: content.horizontalCenter
|
||||
font {
|
||||
pointSize : Style.main.fontSize * Style.pt
|
||||
}
|
||||
color: Style.main.textDisabled
|
||||
text: "\n Current: "+go.fullversion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,4 +12,3 @@ ManualWindow 1.0 ManualWindow.qml
|
||||
OutgoingNoEncPopup 1.0 OutgoingNoEncPopup.qml
|
||||
SettingsView 1.0 SettingsView.qml
|
||||
StatusFooter 1.0 StatusFooter.qml
|
||||
VersionInfo 1.0 VersionInfo.qml
|
||||
|
||||
@ -54,13 +54,15 @@ Item {
|
||||
onWarningFlagsChanged : {
|
||||
if (gui.warningFlags==Style.okInfoBar) {
|
||||
go.normalSystray()
|
||||
} else {
|
||||
if ((gui.warningFlags & Style.errorInfoBar) == Style.errorInfoBar) {
|
||||
go.errorSystray()
|
||||
} else {
|
||||
go.highlightSystray()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ((gui.warningFlags & Style.errorInfoBar) == Style.errorInfoBar) {
|
||||
go.errorSystray()
|
||||
return
|
||||
}
|
||||
|
||||
go.highlightSystray()
|
||||
}
|
||||
|
||||
// Signals from Go
|
||||
@ -112,14 +114,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onRunCheckVersion : {
|
||||
gui.openMainWindow(false)
|
||||
go.setUpdateState("upToDate")
|
||||
winMain.dialogGlobal.state="checkUpdates"
|
||||
winMain.dialogGlobal.show()
|
||||
go.isNewVersionAvailable(showMessage)
|
||||
}
|
||||
|
||||
onSetUpdateState : {
|
||||
// once app is outdated prevent from state change
|
||||
if (winMain.updateState != "forceUpdate") {
|
||||
@ -134,15 +128,49 @@ Item {
|
||||
go.silentBubble(2,qsTr("You have the latest version!", "notification", -1))
|
||||
}
|
||||
|
||||
onNotifyUpdate : {
|
||||
onNotifyManualUpdate: {
|
||||
go.setUpdateState("oldVersion")
|
||||
}
|
||||
|
||||
onNotifyManualUpdateRestartNeeded: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateRestart")
|
||||
winMain.dialogUpdate.finished(false)
|
||||
|
||||
// after manual update - just retart immidiatly
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
}
|
||||
|
||||
onNotifyManualUpdateError: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateError")
|
||||
winMain.dialogUpdate.finished(true)
|
||||
}
|
||||
|
||||
onNotifyForceUpdate : {
|
||||
go.setUpdateState("forceUpdate")
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
go.runCheckVersion(false)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
}
|
||||
|
||||
onNotifySilentUpdateRestartNeeded: {
|
||||
go.setUpdateState("updateRestart")
|
||||
}
|
||||
|
||||
onNotifySilentUpdateError: {
|
||||
go.setUpdateState("updateError")
|
||||
gui.openMainWindow(true)
|
||||
}
|
||||
|
||||
onNotifyLogout : {
|
||||
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Bridge with this account.").arg(accname) )
|
||||
}
|
||||
@ -229,25 +257,16 @@ Item {
|
||||
outgoingNoEncPopup.y = y
|
||||
}
|
||||
|
||||
onUpdateFinished : {
|
||||
winMain.dialogUpdate.finished(hasError)
|
||||
}
|
||||
|
||||
onShowCertIssue : {
|
||||
winMain.tlsBarState="notOK"
|
||||
}
|
||||
|
||||
onOpenReleaseNotesExternally: {
|
||||
Qt.openUrlExternally(go.updateReleaseNotesLink)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: checkVersionTimer
|
||||
repeat : true
|
||||
triggeredOnStart: false
|
||||
interval : Style.main.verCheckRepeatTime
|
||||
onTriggered : go.runCheckVersion(false)
|
||||
}
|
||||
|
||||
function openMainWindow(showAndRise) {
|
||||
// wait and check until font is loaded
|
||||
while(true){
|
||||
@ -287,6 +306,15 @@ Item {
|
||||
winMain.bubbleNote.show()
|
||||
}
|
||||
|
||||
function openReleaseNotes(){
|
||||
if (go.updateReleaseNotesLink == "") {
|
||||
go.checkAndOpenReleaseNotes()
|
||||
return
|
||||
}
|
||||
go.openReleaseNotesExternally()
|
||||
}
|
||||
|
||||
|
||||
// On start
|
||||
Component.onCompleted : {
|
||||
// set messages for translations
|
||||
@ -301,15 +329,8 @@ Item {
|
||||
|
||||
// start window
|
||||
gui.openMainWindow(false)
|
||||
checkVersionTimer.start()
|
||||
if (go.isShownOnStart) {
|
||||
gui.winMain.showAndRise()
|
||||
}
|
||||
go.runCheckVersion(false)
|
||||
|
||||
if (go.isFreshVersion) {
|
||||
go.getLocalVersionInfo()
|
||||
gui.winMain.dialogVersionInfo.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ Item {
|
||||
property var allMonths : getMonthList(1,12)
|
||||
property var allDays : getDayList(1,31)
|
||||
|
||||
property var enums : JSON.parse('{"pathOK":1,"pathEmptyPath":2,"pathWrongPath":4,"pathNotADir":8,"pathWrongPermissions":16,"pathDirEmpty":32,"errUnknownError":0,"errEventAPILogout":1,"errUpdateAPI":2,"errUpdateJSON":3,"errUserAuth":4,"errQApplication":18,"errEmailExportFailed":6,"errEmailExportMissing":7,"errNothingToImport":8,"errEmailImportFailed":12,"errDraftImportFailed":13,"errDraftLabelFailed":14,"errEncryptMessageAttachment":15,"errEncryptMessage":16,"errNoInternetWhileImport":17,"errUnlockUser":5,"errSourceMessageNotSelected":19,"errCannotParseMail":5000,"errWrongLoginOrPassword":5001,"errWrongServerPathOrPort":5002,"errWrongAuthMethod":5003,"errIMAPFetchFailed":5004,"errLocalSourceLoadFailed":1000,"errPMLoadFailed":1001,"errRemoteSourceLoadFailed":1002,"errLoadAccountList":1005,"errExit":1006,"errRetry":1007,"errAsk":1008,"errImportFailed":1009,"errCreateLabelFailed":1010,"errCreateFolderFailed":1011,"errUpdateLabelFailed":1012,"errUpdateFolderFailed":1013,"errFillFolderName":1014,"errSelectFolderColor":1015,"errNoInternet":1016,"folderTypeSystem":"system","folderTypeLabel":"label","folderTypeFolder":"folder","folderTypeExternal":"external","progressInit":"init","progressLooping":"looping","statusNoInternet":"noInternet","statusCheckingInternet":"internetCheck","statusNewVersionAvailable":"oldVersion","statusUpToDate":"upToDate","statusForceUpdate":"forceupdate"}')
|
||||
property var enums : JSON.parse('{"pathOK":1,"pathEmptyPath":2,"pathWrongPath":4,"pathNotADir":8,"pathWrongPermissions":16,"pathDirEmpty":32,"errUnknownError":0,"errEventAPILogout":1,"errUpdateAPI":2,"errUpdateJSON":3,"errUserAuth":4,"errQApplication":18,"errEmailExportFailed":6,"errEmailExportMissing":7,"errNothingToImport":8,"errEmailImportFailed":12,"errDraftImportFailed":13,"errDraftLabelFailed":14,"errEncryptMessageAttachment":15,"errEncryptMessage":16,"errNoInternetWhileImport":17,"errUnlockUser":5,"errSourceMessageNotSelected":19,"errCannotParseMail":5000,"errWrongLoginOrPassword":5001,"errWrongServerPathOrPort":5002,"errWrongAuthMethod":5003,"errIMAPFetchFailed":5004,"errLocalSourceLoadFailed":1000,"errPMLoadFailed":1001,"errRemoteSourceLoadFailed":1002,"errLoadAccountList":1005,"errExit":1006,"errRetry":1007,"errAsk":1008,"errImportFailed":1009,"errCreateLabelFailed":1010,"errCreateFolderFailed":1011,"errUpdateLabelFailed":1012,"errUpdateFolderFailed":1013,"errFillFolderName":1014,"errSelectFolderColor":1015,"errNoInternet":1016,"folderTypeSystem":"system","folderTypeLabel":"label","folderTypeFolder":"folder","folderTypeExternal":"external","progressInit":"init","progressLooping":"looping","statusNoInternet":"noInternet","statusCheckingInternet":"internetCheck","statusNewVersionAvailable":"oldVersion","statusUpToDate":"upToDate","statusForceUpdate":"forceUpdate"}')
|
||||
|
||||
IEStyle{}
|
||||
|
||||
@ -69,20 +69,9 @@ Item {
|
||||
Connections {
|
||||
target: go
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
onShowWindow : {
|
||||
winMain.showAndRise()
|
||||
}
|
||||
|
||||
onProcessFinished : {
|
||||
winMain.dialogAddUser.hide()
|
||||
@ -114,16 +103,9 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onRunCheckVersion : {
|
||||
go.setUpdateState(gui.enums.statusUpToDate)
|
||||
winMain.dialogGlobal.state=gui.enums.statusCheckingInternet
|
||||
winMain.dialogGlobal.show()
|
||||
go.isNewVersionAvailable(showMessage)
|
||||
}
|
||||
|
||||
onSetUpdateState : {
|
||||
// once app is outdated prevent from state change
|
||||
if (winMain.updateState != gui.enums.statusForceUpdate) {
|
||||
if (winMain.updateState != "forceUpdate") {
|
||||
winMain.updateState = updateState
|
||||
}
|
||||
}
|
||||
@ -224,13 +206,43 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onNotifyUpdate : {
|
||||
go.setUpdateState("forceUpdate")
|
||||
onNotifyManualUpdate: {
|
||||
go.setUpdateState("oldVersion")
|
||||
}
|
||||
|
||||
onNotifyManualUpdateRestartNeeded: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
go.runCheckVersion(false)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateRestart")
|
||||
winMain.dialogUpdate.finished(false)
|
||||
|
||||
// after manual update - just retart immidiatly
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
}
|
||||
|
||||
onNotifyManualUpdateError: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateError")
|
||||
winMain.dialogUpdate.finished(true)
|
||||
}
|
||||
|
||||
onNotifyForceUpdate : {
|
||||
go.setUpdateState("forceUpdate")
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
}
|
||||
|
||||
onNotifySilentUpdateRestartNeeded: {
|
||||
go.setUpdateState("updateRestart")
|
||||
}
|
||||
|
||||
onNotifySilentUpdateError: {
|
||||
go.setUpdateState("updateError")
|
||||
}
|
||||
|
||||
onNotifyLogout : {
|
||||
@ -280,6 +292,12 @@ Item {
|
||||
onUpdateFinished : {
|
||||
winMain.dialogUpdate.finished(hasError)
|
||||
}
|
||||
|
||||
onOpenReleaseNotesExternally: {
|
||||
Qt.openUrlExternally(go.updateReleaseNotesLink)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function folderIcon(folderName, folderType) { // translations
|
||||
@ -393,14 +411,15 @@ Item {
|
||||
}
|
||||
*/
|
||||
|
||||
Timer {
|
||||
id: checkVersionTimer
|
||||
repeat : true
|
||||
triggeredOnStart: false
|
||||
interval : Style.main.verCheckRepeatTime
|
||||
onTriggered : go.runCheckVersion(false)
|
||||
function openReleaseNotes(){
|
||||
if (go.updateReleaseNotesLink == "") {
|
||||
go.checkAndOpenReleaseNotes()
|
||||
return
|
||||
}
|
||||
go.openReleaseNotesExternally()
|
||||
}
|
||||
|
||||
|
||||
property string areYouSureYouWantToQuit : qsTr("There are incomplete processes - some items are not yet transferred. Do you really want to stop and quit?")
|
||||
// On start
|
||||
Component.onCompleted : {
|
||||
@ -413,9 +432,6 @@ Item {
|
||||
go.bugNotSent = qsTr("Unable to submit bug report." , "notification", -1)
|
||||
go.bugReportSent = qsTr("Bug report successfully sent." , "notification", -1)
|
||||
|
||||
go.runCheckVersion(false)
|
||||
checkVersionTimer.start()
|
||||
|
||||
gui.allMonths = getMonthList(1,12)
|
||||
gui.allMonthsChanged()
|
||||
}
|
||||
|
||||
@ -314,7 +314,6 @@ Dialog {
|
||||
// hide all other dialogs
|
||||
winMain.dialogAddUser .visible = false
|
||||
winMain.dialogCredits .visible = false
|
||||
//winMain.dialogVersionInfo .visible = false
|
||||
// dialogFirstStart should reappear again after closing global
|
||||
root.visible = true
|
||||
}
|
||||
@ -342,7 +341,7 @@ Dialog {
|
||||
if ( state == "toggleAutoStart" ) { go.toggleAutoStart () }
|
||||
if ( state == "quit" ) { Qt.quit () }
|
||||
if ( state == "instance exists" ) { Qt.quit () }
|
||||
if ( state == "checkUpdates" ) { go.runCheckVersion (true) }
|
||||
if ( state == "checkUpdates" ) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -55,9 +55,7 @@ Item {
|
||||
rightIcon.text : Style.fa.chevron_circle_right
|
||||
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
|
||||
onClicked: {
|
||||
dialogGlobal.state="checkUpdates"
|
||||
dialogGlobal.show()
|
||||
dialogGlobal.confirmed()
|
||||
go.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +113,7 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked : {
|
||||
go.openLicenseFile()
|
||||
go.openLicenseFile()
|
||||
}
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
@ -129,10 +127,7 @@ Item {
|
||||
font.underline: true
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked : {
|
||||
go.getLocalVersionInfo()
|
||||
winMain.dialogVersionInfo.show()
|
||||
}
|
||||
onClicked : gui.openReleaseNotes()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,6 @@ Window {
|
||||
property alias dialogAddUser : dialogAddUser
|
||||
property alias dialogGlobal : dialogGlobal
|
||||
property alias dialogCredits : dialogCredits
|
||||
property alias dialogVersionInfo : dialogVersionInfo
|
||||
property alias dialogUpdate : dialogUpdate
|
||||
property alias popupMessage : popupMessage
|
||||
property alias popupFolderEdit : popupFolderEdit
|
||||
@ -56,12 +55,11 @@ Window {
|
||||
minimumWidth : Style.main.width
|
||||
minimumHeight : Style.main.height
|
||||
|
||||
property bool isOutdateVersion : root.updateState == "forceUpgrade"
|
||||
property bool isOutdateVersion : root.updateState == "forceUpdate"
|
||||
|
||||
property bool activeContent :
|
||||
!dialogAddUser .visible &&
|
||||
!dialogCredits .visible &&
|
||||
!dialogVersionInfo .visible &&
|
||||
!dialogGlobal .visible &&
|
||||
!dialogUpdate .visible &&
|
||||
!dialogImport .visible &&
|
||||
@ -254,40 +252,7 @@ Window {
|
||||
|
||||
DialogUpdate {
|
||||
id: dialogUpdate
|
||||
|
||||
title: root.isOutdateVersion ?
|
||||
qsTr("%1 is outdated", "title of outdate dialog").arg(go.programTitle):
|
||||
qsTr("%1 update to %2", "title of update dialog").arg(go.programTitle).arg(go.newversion)
|
||||
introductionText: {
|
||||
if (root.isOutdateVersion) {
|
||||
if (go.goos=="linux") {
|
||||
return qsTr('You are using an outdated version of our software.<br>
|
||||
Please dowload and install the latest version to continue using %1.<br><br>
|
||||
<a href="%2">%2</a>',
|
||||
"Message for force-update in Linux").arg(go.programTitle).arg(go.landingPage)
|
||||
} else {
|
||||
return qsTr('You are using an outdated version of our software.<br>
|
||||
Please dowload and install the latest version to continue using %1.<br><br>
|
||||
You can continue with update or download and install the new version manually from<br><br>
|
||||
<a href="%2">%2</a>',
|
||||
"Message for force-update in Win/Mac").arg(go.programTitle).arg(go.landingPage)
|
||||
}
|
||||
} else {
|
||||
if (go.goos=="linux") {
|
||||
return qsTr('A new version of %1 is available.<br>
|
||||
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
|
||||
Use your package manager to update or download and install new the version manually from<br><br>
|
||||
<a href="%4">%4</a>',
|
||||
"Message for update in Linux").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
||||
} else {
|
||||
return qsTr('A new version of %1 is available.<br>
|
||||
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
|
||||
You can continue with update or download and install new the version manually from<br><br>
|
||||
<a href="%4">%4</a>',
|
||||
"Message for update in Win/Mac").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
forceUpdate: root.isOutdateVersion
|
||||
}
|
||||
|
||||
|
||||
@ -327,31 +292,6 @@ Window {
|
||||
Credits { }
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: dialogVersionInfo
|
||||
anchors {
|
||||
top : infoBar.bottom
|
||||
bottomMargin: innerWindowBorder
|
||||
leftMargin: innerWindowBorder
|
||||
rightMargin: innerWindowBorder
|
||||
}
|
||||
property bool checkVersion : false
|
||||
title: qsTr("Information about", "title of release notes page") + " v" + go.newversion
|
||||
VersionInfo { }
|
||||
onShow : {
|
||||
// Hide information bar with olde version
|
||||
if ( infoBar.state=="oldVersion" ) {
|
||||
infoBar.state="upToDate"
|
||||
dialogVersionInfo.checkVersion = true
|
||||
}
|
||||
}
|
||||
onHide : {
|
||||
// Reload current version based on online status
|
||||
if (dialogVersionInfo.checkVersion) go.runCheckVersion(false)
|
||||
dialogVersionInfo.checkVersion = false
|
||||
}
|
||||
}
|
||||
|
||||
DialogYesNo {
|
||||
id: dialogGlobal
|
||||
question : ""
|
||||
|
||||
@ -20,17 +20,38 @@
|
||||
import QtQuick 2.8
|
||||
import ProtonUI 1.0
|
||||
import ImportExportUI 1.0
|
||||
import QtQuick.Controls 2.4
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// must have wrapper
|
||||
Rectangle {
|
||||
ScrollView {
|
||||
id: wrapper
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: Style.main.background
|
||||
background: Rectangle {
|
||||
color: Style.main.background
|
||||
}
|
||||
|
||||
// horizontall scrollbar sometimes showes up when vertical scrollbar coveres content
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
|
||||
// keeping vertical scrollbar allways visible when needed
|
||||
Connections {
|
||||
target: wrapper.ScrollBar.vertical
|
||||
onSizeChanged: {
|
||||
// ScrollBar.size == 0 at creating so no need to make it active
|
||||
if (wrapper.ScrollBar.vertical.size < 1.0 && wrapper.ScrollBar.vertical.size > 0 && !wrapper.ScrollBar.vertical.active) {
|
||||
wrapper.ScrollBar.vertical.active = true
|
||||
}
|
||||
}
|
||||
onActiveChanged: {
|
||||
wrapper.ScrollBar.vertical.active = true
|
||||
}
|
||||
}
|
||||
|
||||
// content
|
||||
Column {
|
||||
@ -75,6 +96,25 @@ Item {
|
||||
onClicked: bugreportWin.show()
|
||||
}
|
||||
|
||||
ButtonIconText {
|
||||
id: autoUpdates
|
||||
text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates")
|
||||
leftIcon.text : Style.fa.download
|
||||
rightIcon {
|
||||
font.pointSize : Style.settings.toggleSize * Style.pt
|
||||
text : go.isAutoUpdate!=false ? Style.fa.toggle_on : Style.fa.toggle_off
|
||||
color : go.isAutoUpdate!=false ? Style.main.textBlue : Style.main.textDisabled
|
||||
}
|
||||
Accessible.description: (
|
||||
go.isAutoUpdate == false ?
|
||||
qsTr("Enable" , "Click to enable the automatic update of Bridge") :
|
||||
qsTr("Disable" , "Click to disable the automatic update of Bridge")
|
||||
) + " " + text
|
||||
onClicked: {
|
||||
go.toggleAutoUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
ButtonIconText {
|
||||
|
||||
@ -1,125 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
// credits
|
||||
|
||||
import QtQuick 2.8
|
||||
import ProtonUI 1.0
|
||||
import ImportExportUI 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
Rectangle {
|
||||
id: wrapper
|
||||
anchors.centerIn: parent
|
||||
width: 2*Style.main.width/3
|
||||
height: Style.main.height - 6*Style.dialog.titleSize
|
||||
color: "transparent"
|
||||
|
||||
Flickable {
|
||||
anchors.fill : wrapper
|
||||
contentWidth : wrapper.width
|
||||
contentHeight : content.height
|
||||
flickableDirection : Flickable.VerticalFlick
|
||||
clip : true
|
||||
|
||||
|
||||
Column {
|
||||
id: content
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: wrapper.width
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
visible: go.changelog != ""
|
||||
anchors {
|
||||
left: parent.left
|
||||
}
|
||||
font.bold: true
|
||||
font.pointSize: Style.main.fontSize * Style.pt
|
||||
color: Style.main.text
|
||||
text: qsTr("Release notes:")
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Style.main.leftMargin
|
||||
}
|
||||
font.pointSize: Style.main.fontSize * Style.pt
|
||||
width: wrapper.width - anchors.leftMargin
|
||||
wrapMode: Text.Wrap
|
||||
color: Style.main.text
|
||||
text: go.changelog
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: go.bugfixes != ""
|
||||
anchors {
|
||||
left: parent.left
|
||||
}
|
||||
font.bold: true
|
||||
font.pointSize: Style.main.fontSize * Style.pt
|
||||
color: Style.main.text
|
||||
text: qsTr("Fixed bugs:")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
anchors.fill: parent
|
||||
model: go.bugfixes.split(";")
|
||||
|
||||
Text {
|
||||
visible: go.bugfixes!=""
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Style.main.leftMargin
|
||||
}
|
||||
font.pointSize: Style.main.fontSize * Style.pt
|
||||
width: wrapper.width - anchors.leftMargin
|
||||
wrapMode: Text.Wrap
|
||||
color: Style.main.text
|
||||
text: modelData
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle{id:spacer; color:"transparent"; width:10; height: buttonClose.height}
|
||||
|
||||
|
||||
ButtonRounded {
|
||||
id: buttonClose
|
||||
anchors.horizontalCenter: content.horizontalCenter
|
||||
text: "Close"
|
||||
onClicked: {
|
||||
root.parent.hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AccessibleSelectableText {
|
||||
anchors.horizontalCenter: content.horizontalCenter
|
||||
font {
|
||||
pointSize : Style.main.fontSize * Style.pt
|
||||
}
|
||||
color: Style.main.textDisabled
|
||||
text: "\n Current: "+go.fullversion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,16 +25,17 @@ import ProtonUI 1.0
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
title: "Bridge update "+go.newversion
|
||||
|
||||
property alias introductionText : introduction.text
|
||||
property bool hasError : false
|
||||
property bool forceUpdate : false
|
||||
|
||||
signal cancel()
|
||||
signal okay()
|
||||
|
||||
title: forceUpdate ?
|
||||
qsTr("Update %1 now", "title of force update dialog").arg(go.programTitle):
|
||||
qsTr("Update to %1 %2", "title of normal update dialog").arg(go.programTitle).arg(go.updateVersion)
|
||||
|
||||
isDialogBusy: currentIndex==1
|
||||
isDialogBusy: currentIndex==1 || forceUpdate
|
||||
|
||||
Rectangle { // 0: Release notes and confirm
|
||||
width: parent.width
|
||||
@ -51,24 +52,46 @@ Dialog {
|
||||
color: Style.dialog.text
|
||||
linkColor: Style.dialog.textBlue
|
||||
font {
|
||||
pointSize: 0.8 * Style.dialog.fontSize * Style.pt
|
||||
pointSize: Style.dialog.fontSize * Style.pt
|
||||
}
|
||||
width: 2*root.width/3
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
// customize message per application
|
||||
text: ' <a href="%1">Release notes</a><br> New version %2<br> <br><br> <a href="%3">%3</a>'
|
||||
text: {
|
||||
if (forceUpdate) {
|
||||
if (go.updateCanInstall) {
|
||||
return qsTr('You need to update this app to continue using it.<br>
|
||||
Update now or manually download the most recent version here:<br>
|
||||
<a href="%1">%1</a><br>
|
||||
<a href="https://protonmail.com/support/knowledge-base/update-required/">Learn why</a> you need to update',
|
||||
"Message for force-update").arg(go.updateLandingPage)
|
||||
} else {
|
||||
return qsTr('You need to update this app to continue using it.<br>
|
||||
Download the most recent version here:<br>
|
||||
<a href="%1">%1</a><br>
|
||||
<a href="https://protonmail.com/support/knowledge-base/update-required/">Learn why</a> you need to update',
|
||||
"Message for force-update").arg(go.updateLandingPage)
|
||||
}
|
||||
}
|
||||
|
||||
if (go.updateCanInstall) {
|
||||
return qsTr('Update to the newest version or download it from:<br>
|
||||
<a href="%1">%1</a><br>
|
||||
<a href="%2">View release notes</a>',
|
||||
"Message for manual update").arg(go.updateLandingPage).arg(go.updateReleaseNotesLink)
|
||||
} else {
|
||||
return qsTr('Update to the newest version from:<br>
|
||||
<a href="%1">%1</a><br>
|
||||
<a href="%2">View release notes</a>',
|
||||
"Message for manual update").arg(go.updateLandingPage).arg(go.updateReleaseNotesLink)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onLinkActivated : {
|
||||
console.log("clicked link:", link)
|
||||
if (link == "releaseNotes"){
|
||||
root.hide()
|
||||
winMain.dialogVersionInfo.show()
|
||||
} else {
|
||||
root.hide()
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@ -78,21 +101,30 @@ Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
CheckBoxLabel {
|
||||
id: autoUpdate
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Automatically update in the future", "Checkbox label for using autoupdates later on")
|
||||
checked: go.isAutoUpdate
|
||||
onToggled: go.toggleAutoUpdate()
|
||||
visible: !root.forceUpdate
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.dialog.spacing
|
||||
|
||||
ButtonRounded {
|
||||
fa_icon: Style.fa.times
|
||||
text: (go.goos=="linux" ? qsTr("Okay") : qsTr("Cancel"))
|
||||
text: root.forceUpdate ? qsTr("Quit") : qsTr("Cancel")
|
||||
color_main: Style.dialog.text
|
||||
onClicked: root.cancel()
|
||||
onClicked: root.forceUpdate ? Qt.quit() : root.cancel()
|
||||
}
|
||||
|
||||
ButtonRounded {
|
||||
fa_icon: Style.fa.check
|
||||
text: qsTr("Update")
|
||||
visible: go.goos!="linux"
|
||||
visible: go.updateCanInstall
|
||||
color_main: Style.dialog.text
|
||||
color_minor: Style.main.textBlue
|
||||
isOpaque: true
|
||||
@ -102,7 +134,7 @@ Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // 0: Check / download / unpack / prepare
|
||||
Rectangle { // 1: Installing update
|
||||
id: updateStatus
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
@ -121,35 +153,29 @@ Dialog {
|
||||
width: 2*root.width/3
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
text: {
|
||||
switch (go.progressDescription) {
|
||||
case "1": return qsTr("Checking the current version.")
|
||||
case "2": return qsTr("Downloading the update files.")
|
||||
case "3": return qsTr("Verifying the update files.")
|
||||
case "4": return qsTr("Unpacking the update files.")
|
||||
case "5": return qsTr("Starting the update.")
|
||||
case "6": return qsTr("Quitting the application.")
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
text: qsTr("Updating...")
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
id: progressbar
|
||||
implicitWidth : 2*updateStatus.width/3
|
||||
implicitHeight : Style.exporting.rowHeight
|
||||
visible: go.progress!=0 // hack hide animation when clearing out progress bar
|
||||
value: go.progress
|
||||
property int current: go.total * go.progress
|
||||
property bool isFinished: finishedPartBar.width == progressbar.width
|
||||
id: updateProgressBar
|
||||
width: 2*updateStatus.width/3
|
||||
height: Style.exporting.rowHeight
|
||||
//implicitWidth : 2*updateStatus.width/3
|
||||
//implicitHeight : Style.exporting.rowHeight
|
||||
indeterminate: true
|
||||
//value: 0.5
|
||||
//property int current: go.total * go.progress
|
||||
//property bool isFinished: finishedPartBar.width == progressbar.width
|
||||
background: Rectangle {
|
||||
radius : Style.exporting.boxRadius
|
||||
color : Style.exporting.progressBackground
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
clip: true
|
||||
Rectangle {
|
||||
id: finishedPartBar
|
||||
width : parent.width * progressbar.visualPosition
|
||||
id: progressIndicator
|
||||
width : updateProgressBar.indeterminate ? 50 : parent.width * updateProgressBar.visualPosition
|
||||
height : parent.height
|
||||
radius : Style.exporting.boxRadius
|
||||
gradient : Gradient {
|
||||
@ -161,6 +187,27 @@ Dialog {
|
||||
Behavior on width {
|
||||
NumberAnimation { duration:300; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
running: updateProgressBar.visible && updateProgressBar.indeterminate
|
||||
loops: Animation.Infinite
|
||||
|
||||
SmoothedAnimation {
|
||||
target: progressIndicator
|
||||
property: "x"
|
||||
from: 0
|
||||
to: updateProgressBar.width - progressIndicator.width
|
||||
duration: 2000
|
||||
}
|
||||
|
||||
SmoothedAnimation {
|
||||
target: progressIndicator
|
||||
property: "x"
|
||||
from: updateProgressBar.width - progressIndicator.width
|
||||
to: 0
|
||||
duration: 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
@ -175,7 +222,7 @@ Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // 1: Something went wrong / All ok, closing bridge
|
||||
Rectangle { // 2: Something went wrong / All ok, closing bridge
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: Style.transparent
|
||||
@ -193,8 +240,8 @@ Dialog {
|
||||
width: 2*root.width/3
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
text: !root.hasError ? qsTr('Application will quit now to finish the update.', "message after successful update") :
|
||||
qsTr('<b>The update procedure was not successful!</b><br>Please follow the download link and update manually. <br><br><a href="%1">%1</a>').arg(go.downloadLink)
|
||||
text: !root.hasError ? qsTr('%1 will restart now to finish the update.', "message after successful update").arg(go.programTitle) :
|
||||
qsTr('<b>The update procedure was not successful!</b><br>Please follow the download link and update manually. <br><br><a href="%1">%1</a>').arg(go.updateLandingPage)
|
||||
|
||||
onLinkActivated : {
|
||||
console.log("clicked link:", link)
|
||||
@ -225,7 +272,7 @@ Dialog {
|
||||
|
||||
function finished(hasError) {
|
||||
root.hasError = hasError
|
||||
root.incrementCurrentIndex()
|
||||
root.currentIndex = 2
|
||||
}
|
||||
|
||||
onShow: {
|
||||
@ -239,9 +286,10 @@ Dialog {
|
||||
onOkay: {
|
||||
switch (root.currentIndex) {
|
||||
case 0:
|
||||
go.startUpdate()
|
||||
go.startManualUpdate()
|
||||
root.currentIndex = 1
|
||||
break
|
||||
}
|
||||
root.incrementCurrentIndex()
|
||||
}
|
||||
|
||||
onCancel: {
|
||||
|
||||
@ -53,6 +53,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Row {
|
||||
id: messageRow
|
||||
anchors.centerIn: root
|
||||
visible: root.isVisible
|
||||
spacing: Style.main.leftMarginButton
|
||||
@ -63,80 +64,74 @@ Rectangle {
|
||||
}
|
||||
|
||||
ClickIconText {
|
||||
id: linkText
|
||||
anchors.verticalCenter : message.verticalCenter
|
||||
text : "("+go.newversion+" " + qsTr("release notes", "display the release notes from the new version")+")"
|
||||
visible : root.state=="oldVersion" && ( go.changelog!="" || go.bugfixes!="")
|
||||
iconText : ""
|
||||
onClicked : {
|
||||
dialogVersionInfo.show()
|
||||
}
|
||||
fontSize : root.fontSize
|
||||
}
|
||||
|
||||
ClickIconText {
|
||||
id: actionText
|
||||
anchors.verticalCenter : message.verticalCenter
|
||||
text : root.state=="oldVersion" || root.state == "forceUpdate" ?
|
||||
qsTr("Update", "click to update to a new version when one is available") :
|
||||
qsTr("Retry now", "click to try to connect to the internet when the app is disconnected from the internet")
|
||||
visible : root.state!="internetCheck"
|
||||
iconText : ""
|
||||
onClicked : {
|
||||
if (root.state=="oldVersion" || root.state=="forceUpdate" ) {
|
||||
winMain.dialogUpdate.show()
|
||||
} else {
|
||||
go.checkInternet()
|
||||
}
|
||||
}
|
||||
fontSize : root.fontSize
|
||||
textUnderline: true
|
||||
}
|
||||
Text {
|
||||
id: separatorText
|
||||
anchors.baseline : message.baseline
|
||||
color: Style.main.text
|
||||
font {
|
||||
pointSize : root.fontSize * Style.pt
|
||||
bold : true
|
||||
}
|
||||
visible: root.state=="oldVersion" || root.state=="noInternet"
|
||||
text : "|"
|
||||
}
|
||||
ClickIconText {
|
||||
id: action2Text
|
||||
anchors.verticalCenter : message.verticalCenter
|
||||
iconText : ""
|
||||
text : root.state == "noInternet" ?
|
||||
qsTr("Troubleshoot", "Show modal screen with additional tips for troubleshooting connection issues") :
|
||||
qsTr("Remind me later", "Do not install new version and dismiss a notification")
|
||||
visible : root.state=="oldVersion" || root.state=="noInternet"
|
||||
onClicked : {
|
||||
if (root.state == "oldVersion") {
|
||||
root.state = "upToDate"
|
||||
}
|
||||
if (root.state == "noInternet") {
|
||||
dialogConnectionTroubleshoot.show()
|
||||
}
|
||||
}
|
||||
fontSize : root.fontSize
|
||||
textUnderline: true
|
||||
}
|
||||
}
|
||||
|
||||
ClickIconText {
|
||||
id: closeSign
|
||||
anchors.verticalCenter : messageRow.verticalCenter
|
||||
anchors.right: root.right
|
||||
iconText : Style.fa.close
|
||||
fontSize : root.fontSize
|
||||
textUnderline: true
|
||||
}
|
||||
|
||||
onStateChanged : {
|
||||
switch (root.state) {
|
||||
case "forceUpdate" :
|
||||
gui.warningFlags |= Style.errorInfoBar
|
||||
break;
|
||||
case "upToDate" :
|
||||
gui.warningFlags &= ~Style.warnInfoBar
|
||||
iTry = 0
|
||||
secLeft=checkInterval[iTry]
|
||||
case "internetCheck":
|
||||
break;
|
||||
case "noInternet" :
|
||||
gui.warningFlags |= Style.warnInfoBar
|
||||
retryInternet.start()
|
||||
secLeft=checkInterval[iTry]
|
||||
break;
|
||||
default :
|
||||
case "oldVersion":
|
||||
gui.warningFlags |= Style.warnInfoBar
|
||||
break;
|
||||
case "forceUpdate":
|
||||
gui.warningFlags |= Style.errorInfoBar
|
||||
break;
|
||||
case "upToDate":
|
||||
gui.warningFlags &= ~Style.warnInfoBar
|
||||
iTry = 0
|
||||
secLeft=checkInterval[iTry]
|
||||
break;
|
||||
case "updateRestart":
|
||||
gui.warningFlags |= Style.warnInfoBar
|
||||
break;
|
||||
case "updateError":
|
||||
gui.warningFlags |= Style.errorInfoBar
|
||||
break;
|
||||
default :
|
||||
break;
|
||||
}
|
||||
|
||||
if (root.state!="noInternet") {
|
||||
@ -172,6 +167,26 @@ Rectangle {
|
||||
color: Style.main.background
|
||||
text: qsTr("Checking connection. Please wait...", "displayed after user retries internet connection")
|
||||
}
|
||||
PropertyChanges {
|
||||
target: linkText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: actionText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: separatorText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: action2Text
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: closeSign
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "noInternet"
|
||||
@ -186,6 +201,35 @@ Rectangle {
|
||||
color: Style.main.line
|
||||
text: qsTr("Cannot contact server. Retrying in ", "displayed when the app is disconnected from the internet or server has problems")+timeToRetry()+"."
|
||||
}
|
||||
PropertyChanges {
|
||||
target: linkText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: actionText
|
||||
visible: true
|
||||
text: qsTr("Retry now", "click to try to connect to the internet when the app is disconnected from the internet")
|
||||
onClicked: {
|
||||
go.checkInternet()
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: separatorText
|
||||
visible: true
|
||||
text: "|"
|
||||
}
|
||||
PropertyChanges {
|
||||
target: action2Text
|
||||
visible: true
|
||||
text: qsTr("Troubleshoot", "Show modal screen with additional tips for troubleshooting connection issues")
|
||||
onClicked: {
|
||||
dialogConnectionTroubleshoot.show()
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: closeSign
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "oldVersion"
|
||||
@ -198,7 +242,36 @@ Rectangle {
|
||||
PropertyChanges {
|
||||
target: message
|
||||
color: Style.main.background
|
||||
text: qsTr("An update is available.", "displayed in a notification when an app update is available")
|
||||
text: qsTr("Update available", "displayed in a notification when an app update is available")
|
||||
}
|
||||
PropertyChanges {
|
||||
target: linkText
|
||||
visible: true
|
||||
text: "(" + qsTr("view release notes", "display the release notes from the new version") + ")"
|
||||
onClicked: gui.openReleaseNotes()
|
||||
}
|
||||
PropertyChanges {
|
||||
target: actionText
|
||||
visible: true
|
||||
text: qsTr("Update", "click to update to a new version when one is available")
|
||||
onClicked: {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: separatorText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: action2Text
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: closeSign
|
||||
visible: true
|
||||
onClicked: {
|
||||
root.state = "upToDate"
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
@ -214,6 +287,30 @@ Rectangle {
|
||||
color: Style.main.line
|
||||
text: qsTr("%1 is outdated.", "displayed in a notification when app is outdated").arg(go.programTitle)
|
||||
}
|
||||
PropertyChanges {
|
||||
target: linkText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: actionText
|
||||
visible: true
|
||||
text: qsTr("Update", "click to update to a new version when one is available")
|
||||
onClicked: {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: separatorText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: action2Text
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: closeSign
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "upToDate"
|
||||
@ -228,6 +325,103 @@ Rectangle {
|
||||
color: Style.main.background
|
||||
text: ""
|
||||
}
|
||||
PropertyChanges {
|
||||
target: linkText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: actionText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: separatorText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: action2Text
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: closeSign
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "updateRestart"
|
||||
PropertyChanges {
|
||||
target: root
|
||||
height: 2* Style.main.fontSize
|
||||
isVisible: true
|
||||
color: Style.main.textBlue
|
||||
}
|
||||
PropertyChanges {
|
||||
target: message
|
||||
color: Style.main.background
|
||||
text: qsTr("%1 update is ready", "displayed in a notification when an app update is installed and restart is needed").arg(go.programTitle)
|
||||
}
|
||||
PropertyChanges {
|
||||
target: linkText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: actionText
|
||||
visible: true
|
||||
text: qsTr("Restart now", "click to restart application as new version was installed")
|
||||
onClicked: {
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: separatorText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: action2Text
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: closeSign
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "updateError"
|
||||
PropertyChanges {
|
||||
target: root
|
||||
height: 2* Style.main.fontSize
|
||||
isVisible: true
|
||||
color: Style.main.textRed
|
||||
}
|
||||
PropertyChanges {
|
||||
target: message
|
||||
color: Style.main.line
|
||||
text: qsTr("Sorry, %1 couldn't update.", "displayed in a notification when app failed to autoupdate").arg(go.programTitle)
|
||||
}
|
||||
PropertyChanges {
|
||||
target: linkText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: actionText
|
||||
visible: true
|
||||
text: qsTr("Please update manually", "click to open download page to update manally")
|
||||
onClicked: {
|
||||
Qt.openUrlExternally(go.updateLandingPage)
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: separatorText
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: action2Text
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: closeSign
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -59,7 +59,6 @@ QtObject {
|
||||
property real fontSize : 12 * px
|
||||
property real iconSize : 15 * px
|
||||
property real leftMarginButton : 9 * px
|
||||
property real verCheckRepeatTime : 15*60*60*1000 // milliseconds
|
||||
property real topMargin : fontSize
|
||||
property real bottomMargin : fontSize
|
||||
property real border : 1 * px
|
||||
|
||||
@ -108,9 +108,14 @@ Window {
|
||||
ListElement { title: "Logout bridge" }
|
||||
ListElement { title: "Internet on" }
|
||||
ListElement { title: "Internet off" }
|
||||
ListElement { title: "NeedUpdate" }
|
||||
ListElement { title: "UpToDate" }
|
||||
ListElement { title: "ForceUpdate" }
|
||||
ListElement { title: "NotifyManualUpdate(CanInstall)" }
|
||||
ListElement { title: "NotifyManualUpdate(CantInstall)" }
|
||||
ListElement { title: "NotifyManualUpdateRestart" }
|
||||
ListElement { title: "NotifyManualUpdateError" }
|
||||
ListElement { title: "ForceUpdate" }
|
||||
ListElement { title: "NotifySilentUpdateRestartNeeded" }
|
||||
ListElement { title: "NotifySilentUpdateError" }
|
||||
ListElement { title: "Linux" }
|
||||
ListElement { title: "Windows" }
|
||||
ListElement { title: "Macos" }
|
||||
@ -196,11 +201,28 @@ Window {
|
||||
case "UpToDate" :
|
||||
testroot.newVersion = false
|
||||
break;
|
||||
case "NeedUpdate" :
|
||||
testroot.newVersion = true
|
||||
case "NotifyManualUpdate(CanInstall)" :
|
||||
go.notifyManualUpdate()
|
||||
go.updateCanInstall = true
|
||||
break;
|
||||
case "NotifyManualUpdate(CantInstall)" :
|
||||
go.notifyManualUpdate()
|
||||
go.updateCanInstall = false
|
||||
break;
|
||||
case "NotifyManualUpdateRestart":
|
||||
go.notifyManualUpdateRestartNeeded()
|
||||
break;
|
||||
case "NotifyManualUpdateError":
|
||||
go.notifyManualUpdateError()
|
||||
break;
|
||||
case "ForceUpdate" :
|
||||
go.notifyUpdate()
|
||||
go.notifyForceUpdate()
|
||||
break;
|
||||
case "NotifySilentUpdateRestartNeeded" :
|
||||
go.notifySilentUpdateRestartNeeded()
|
||||
break;
|
||||
case "NotifySilentUpdateError" :
|
||||
go.notifySilentUpdateError()
|
||||
break;
|
||||
case "SendAlertPopup" :
|
||||
go.showOutgoingNoEncPopup("Alert sending unencrypted!")
|
||||
@ -244,6 +266,8 @@ Window {
|
||||
id: go
|
||||
|
||||
property bool isAutoStart : true
|
||||
property bool isAutoUpdate : false
|
||||
property bool isEarlyAccess : false
|
||||
property bool isProxyAllowed : false
|
||||
property bool isFirstStart : false
|
||||
property bool isFreshVersion : false
|
||||
@ -269,19 +293,40 @@ Window {
|
||||
property string genericErrSeeLogs
|
||||
|
||||
property string programTitle : "ProtonMail Bridge"
|
||||
property string newversion : "QA.1.0"
|
||||
property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00"
|
||||
property string landingPage : "https://landing.page"
|
||||
//property string downloadLink: "https://landing.page/download/link"
|
||||
property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;"
|
||||
//property string changelog : "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
|
||||
property string changelog : "• Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients)\n• Notification that outgoing email will be delivered as non-encrypted.\n• NOTE: Due to a change of the keychain format, you will need to add your account(s) to the Bridge after installing this version"
|
||||
property string bugfixes : "• Support accounts with same user names\n• Support sending vCalendar event"
|
||||
|
||||
property string updateVersion : "QA.1.0"
|
||||
property bool updateCanInstall: true
|
||||
property string updateLandingPage : "https://protonmail.com/bridge/download/"
|
||||
property string updateReleaseNotesLink : "" // "https://protonmail.com/download/bridge/release_notes.html"
|
||||
signal notifyManualUpdate()
|
||||
signal notifyManualUpdateRestartNeeded()
|
||||
signal notifyManualUpdateError()
|
||||
signal notifyForceUpdate()
|
||||
signal notifySilentUpdateRestartNeeded()
|
||||
signal notifySilentUpdateError()
|
||||
function checkForUpdates() {
|
||||
console.log("checkForUpdates")
|
||||
}
|
||||
function startManualUpdate() {
|
||||
console.log("startManualUpdate")
|
||||
}
|
||||
function checkAndOpenReleaseNotes() {
|
||||
console.log("check for release notes")
|
||||
go.updateReleaseNotesLink = "https://protonmail.com/download/bridge/release_notes.html"
|
||||
go.openReleaseNotesExternally()
|
||||
}
|
||||
|
||||
|
||||
property string credits : "here;goes;list;;of;;used;packages;"
|
||||
|
||||
property real progress: 0.3
|
||||
property int progressDescription: 2
|
||||
|
||||
function setToRestart() {
|
||||
console.log("setting to restart")
|
||||
}
|
||||
|
||||
signal toggleMainWin(int systX, int systY, int systW, int systH)
|
||||
|
||||
@ -297,18 +342,19 @@ Window {
|
||||
|
||||
signal processFinished()
|
||||
signal toggleAutoStart()
|
||||
signal toggleEarlyAccess()
|
||||
signal toggleAutoUpdate()
|
||||
signal notifyBubble(int tabIndex, string message)
|
||||
signal silentBubble(int tabIndex, string message)
|
||||
signal runCheckVersion(bool showMessage)
|
||||
signal setAddAccountWarning(string message)
|
||||
|
||||
signal notifyUpdate()
|
||||
signal notifyFirewall()
|
||||
signal notifyLogout(string accname)
|
||||
signal notifyAddressChanged(string accname)
|
||||
signal notifyAddressChangedLogout(string accname)
|
||||
signal failedAutostartCode(string code)
|
||||
|
||||
signal openReleaseNotesExternally()
|
||||
signal showCertIssue()
|
||||
|
||||
signal updateFinished(bool hasError)
|
||||
@ -465,9 +511,6 @@ Window {
|
||||
switch (timer.work) {
|
||||
case "wait":
|
||||
break
|
||||
case "startUpdate":
|
||||
go.animateProgressBar.start()
|
||||
go.updateFinished(true)
|
||||
default:
|
||||
go.processFinished()
|
||||
}
|
||||
@ -478,11 +521,6 @@ Window {
|
||||
timer.start()
|
||||
}
|
||||
|
||||
function startUpdate() {
|
||||
timer.work="startUpdate"
|
||||
timer.start()
|
||||
}
|
||||
|
||||
function loadAccounts() {
|
||||
console.log("Test: Account loaded")
|
||||
}
|
||||
@ -502,21 +540,7 @@ Window {
|
||||
}
|
||||
|
||||
function getLocalVersionInfo(){
|
||||
go.newversion = "QA.1.0"
|
||||
}
|
||||
|
||||
function isNewVersionAvailable(showMessage){
|
||||
if (testroot.newVersion) {
|
||||
go.newversion = "QA.2.0"
|
||||
setUpdateState("oldVersion")
|
||||
} else {
|
||||
go.newversion = "QA.1.0"
|
||||
setUpdateState("upToDate")
|
||||
if(showMessage) {
|
||||
notifyVersionIsTheLatest()
|
||||
}
|
||||
}
|
||||
workAndClose()
|
||||
go.updateVersion = "QA.1.0"
|
||||
}
|
||||
|
||||
function getBackendVersion() {
|
||||
@ -566,7 +590,6 @@ Window {
|
||||
return 0
|
||||
}
|
||||
|
||||
property bool isRestarting: false
|
||||
function setPortsAndSecurity(portIMAP, portSMTP, secSMTP) {
|
||||
console.log("Test: ports changed", portIMAP, portSMTP, secSMTP)
|
||||
}
|
||||
@ -606,6 +629,18 @@ Window {
|
||||
isAutoStart = (isAutoStart!=false) ? false : true
|
||||
console.log (" Test: toggleAutoStart "+isAutoStart)
|
||||
}
|
||||
|
||||
onToggleAutoUpdate: {
|
||||
workAndClose()
|
||||
isAutoUpdate = (isAutoUpdate!=false) ? false : true
|
||||
console.log (" Test: onToggleAutoUpdate "+isAutoUpdate)
|
||||
}
|
||||
|
||||
onToggleEarlyAccess: {
|
||||
workAndClose()
|
||||
isEarlyAccess = (isEarlyAccess!=false) ? false : true
|
||||
console.log (" Test: onToggleEarlyAccess "+isEarlyAccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -98,24 +98,29 @@ Window {
|
||||
ListModel {
|
||||
id: buttons
|
||||
|
||||
ListElement { title : "Show window" }
|
||||
ListElement { title : "Logout" }
|
||||
ListElement { title : "Internet on" }
|
||||
ListElement { title : "Internet off" }
|
||||
ListElement { title : "Macos" }
|
||||
ListElement { title : "Windows" }
|
||||
ListElement { title : "Linux" }
|
||||
ListElement { title : "New Version" }
|
||||
ListElement { title : "ForceUpgrade" }
|
||||
ListElement { title : "ImportStructure" }
|
||||
ListElement { title : "DraftImpFailed" }
|
||||
ListElement { title : "NoInterImp" }
|
||||
ListElement { title : "ReportImp" }
|
||||
ListElement { title : "NewFolder" }
|
||||
ListElement { title : "EditFolder" }
|
||||
ListElement { title : "EditLabel" }
|
||||
ListElement { title : "ExpProgErr" }
|
||||
ListElement { title : "ImpProgErr" }
|
||||
ListElement { title : "Show window" }
|
||||
ListElement { title : "Logout" }
|
||||
ListElement { title : "Internet on" }
|
||||
ListElement { title : "Internet off" }
|
||||
ListElement { title : "Macos" }
|
||||
ListElement { title : "Windows" }
|
||||
ListElement { title : "Linux" }
|
||||
ListElement { title: "NotifyManualUpdate(CanInstall)" }
|
||||
ListElement { title: "NotifyManualUpdate(CantInstall)" }
|
||||
ListElement { title: "NotifyManualUpdateRestart" }
|
||||
ListElement { title: "NotifyManualUpdateError" }
|
||||
ListElement { title: "ForceUpdate" }
|
||||
ListElement { title: "NotifySilentUpdateRestartNeeded" }
|
||||
ListElement { title: "NotifySilentUpdateError" }
|
||||
ListElement { title : "ImportStructure" }
|
||||
ListElement { title : "DraftImpFailed" }
|
||||
ListElement { title : "NoInterImp" }
|
||||
ListElement { title : "ReportImp" }
|
||||
ListElement { title : "NewFolder" }
|
||||
ListElement { title : "EditFolder" }
|
||||
ListElement { title : "EditLabel" }
|
||||
ListElement { title : "ExpProgErr" }
|
||||
ListElement { title : "ImpProgErr" }
|
||||
}
|
||||
|
||||
ListView {
|
||||
@ -161,12 +166,28 @@ Window {
|
||||
case "Linux" :
|
||||
go.goos = "linux";
|
||||
break;
|
||||
case "New Version" :
|
||||
testroot.newVersion = !testroot.newVersion
|
||||
systrText.text = testroot.newVersion ? "new version" : "uptodate"
|
||||
break
|
||||
case "ForceUpgrade" :
|
||||
go.notifyUpgrade()
|
||||
case "NotifyManualUpdate(CanInstall)" :
|
||||
go.notifyManualUpdate()
|
||||
go.updateCanInstall = true
|
||||
break;
|
||||
case "NotifyManualUpdate(CantInstall)" :
|
||||
go.notifyManualUpdate()
|
||||
go.updateCanInstall = false
|
||||
break;
|
||||
case "NotifyManualUpdateRestart":
|
||||
go.notifyManualUpdateRestartNeeded()
|
||||
break;
|
||||
case "NotifyManualUpdateError":
|
||||
go.notifyManualUpdateError()
|
||||
break;
|
||||
case "ForceUpdate" :
|
||||
go.notifyForceUpdate()
|
||||
break;
|
||||
case "NotifySilentUpdateRestartNeeded" :
|
||||
go.notifySilentUpdateRestartNeeded()
|
||||
break;
|
||||
case "NotifySilentUpdateError" :
|
||||
go.notifySilentUpdateError()
|
||||
break;
|
||||
case "ImportStructure" :
|
||||
testgui.winMain.dialogImport.address = "cuto@pm.com"
|
||||
@ -815,6 +836,7 @@ Window {
|
||||
id: go
|
||||
|
||||
property int isAutoStart : 1
|
||||
property bool isAutoUpdate : false
|
||||
property bool isFirstStart : false
|
||||
property string currentAddress : "none"
|
||||
//property string goos : "windows"
|
||||
@ -831,12 +853,30 @@ Window {
|
||||
property string bugReportSent
|
||||
|
||||
property string programTitle : "ProtonMail Import-Export app"
|
||||
property string newversion : "q0.1.0"
|
||||
property string landingPage : "https://landing.page"
|
||||
property string changelog : "• Lorem ipsum dolor sit amet\n• consetetur sadipscing elitr,\n• sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,\n• sed diam voluptua.\n• At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
|
||||
//property string changelog : ""
|
||||
property string bugfixes : "• lorem ipsum dolor sit amet;• consetetur sadipscing elitr;• sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat;• sed diam voluptua;• at vero eos et accusam et justo duo dolores et ea rebum;• stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet"
|
||||
//property string bugfixes : ""
|
||||
property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00"
|
||||
property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;"
|
||||
|
||||
property string updateVersion : "q0.1.0"
|
||||
property bool updateCanInstall: true
|
||||
property string updateLandingPage : "https://protonmail.com/import-export/download/"
|
||||
property string updateReleaseNotesLink : "https://protonmail.com/download/ie/release_notes.html"
|
||||
signal notifyManualUpdate()
|
||||
signal notifyManualUpdateRestartNeeded()
|
||||
signal notifyManualUpdateError()
|
||||
signal notifyForceUpdate()
|
||||
signal notifySilentUpdateRestartNeeded()
|
||||
signal notifySilentUpdateError()
|
||||
function checkForUpdates() {
|
||||
console.log("checkForUpdates")
|
||||
}
|
||||
function startManualUpdate() {
|
||||
console.log("startManualUpdate")
|
||||
}
|
||||
function checkAndOpenReleaseNotes() {
|
||||
console.log("check for release notes")
|
||||
go.updateReleaseNotesLink = "https://protonmail.com/download/import-export/release_notes.html"
|
||||
go.openReleaseNotesExternally()
|
||||
}
|
||||
|
||||
property real progress: 0.0
|
||||
property int progressFails: 0
|
||||
@ -849,13 +889,10 @@ Window {
|
||||
|
||||
signal toggleMainWin(int systX, int systY, int systW, int systH)
|
||||
|
||||
|
||||
|
||||
signal notifyHasNoKeychain()
|
||||
signal notifyKeychainRebuild()
|
||||
signal notifyAddressChangedLogout()
|
||||
signal notifyAddressChanged()
|
||||
signal notifyUpdate()
|
||||
|
||||
signal showWindow()
|
||||
signal showHelp()
|
||||
@ -874,17 +911,23 @@ Window {
|
||||
|
||||
signal processFinished()
|
||||
signal toggleAutoStart()
|
||||
signal toggleAutoUpdate()
|
||||
signal notifyBubble(int tabIndex, string message)
|
||||
signal runCheckVersion(bool showMessage)
|
||||
signal setAddAccountWarning(string message)
|
||||
signal notifyUpgrade()
|
||||
signal notifyUpdate()
|
||||
signal updateFinished(bool hasError)
|
||||
|
||||
signal openReleaseNotesExternally()
|
||||
|
||||
signal notifyLogout(string accname)
|
||||
|
||||
signal notifyError(int errCode)
|
||||
property string errorDescription : ""
|
||||
|
||||
function setToRestart() {
|
||||
console.log("setting to restart")
|
||||
}
|
||||
|
||||
function delay(duration) {
|
||||
var timeStart = new Date().getTime();
|
||||
|
||||
@ -958,7 +1001,7 @@ Window {
|
||||
workAndClose("addAccount")
|
||||
}
|
||||
|
||||
property SequentialAnimation animateProgressBarUpgrade : SequentialAnimation {
|
||||
property SequentialAnimation animateProgressBarUpdate : SequentialAnimation {
|
||||
// version
|
||||
PropertyAnimation{ target: go; properties: "progressDescription"; to: 1; duration: 1; }
|
||||
PropertyAnimation{ duration: 2000; }
|
||||
@ -1069,7 +1112,6 @@ Window {
|
||||
onTriggered : {
|
||||
console.log("triggered "+timer.work)
|
||||
switch (timer.work) {
|
||||
case "isNewVersionAvailable" :
|
||||
case "clearCache" :
|
||||
case "clearKeychain" :
|
||||
case "logout" :
|
||||
@ -1096,8 +1138,8 @@ Window {
|
||||
go.animateProgressBar.start()
|
||||
break;
|
||||
|
||||
case "startUpgrade":
|
||||
go.animateProgressBarUpgrade.start()
|
||||
case "startManualUpdate":
|
||||
go.animateProgressBarUpdate.start()
|
||||
go.updateFinished(true)
|
||||
|
||||
default:
|
||||
@ -1108,18 +1150,10 @@ Window {
|
||||
|
||||
function workAndClose(workDescription) {
|
||||
go.progress=0.0
|
||||
timer.work = workDescription
|
||||
timer.work = workDescription === undefined ? "" : workDescription
|
||||
timer.start()
|
||||
}
|
||||
|
||||
function startUpgrade() {
|
||||
timer.work="startUpgrade"
|
||||
timer.start()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function checkPathStatus(path) {
|
||||
if ( path == "" ) return testgui.enums.pathEmptyPath
|
||||
if ( path == "wrong" ) return testgui.enums.pathWrongPath
|
||||
@ -1221,20 +1255,6 @@ Window {
|
||||
workAndClose("switchAddressMode")
|
||||
}
|
||||
|
||||
function isNewVersionAvailable(showMessage){
|
||||
if (testroot.newVersion) {
|
||||
setUpdateState("oldVersion")
|
||||
} else {
|
||||
setUpdateState("upToDate")
|
||||
if(showMessage) {
|
||||
notifyVersionIsTheLatest()
|
||||
}
|
||||
}
|
||||
workAndClose("isNewVersionAvailable")
|
||||
//notifyBubble(2,go.versionCheckFailed)
|
||||
return 0
|
||||
}
|
||||
|
||||
function getLocalVersionInfo(){}
|
||||
|
||||
function getBackendVersion() {
|
||||
@ -1331,5 +1351,11 @@ Window {
|
||||
console.log("sending import report from ", address, " file ", fname)
|
||||
return !fname.includes("fail")
|
||||
}
|
||||
|
||||
onToggleAutoUpdate: {
|
||||
workAndClose()
|
||||
isAutoUpdate = (isAutoUpdate!=false) ? false : true
|
||||
console.log (" Test: onToggleAutoUpdate "+isAutoUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,10 +25,9 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
@ -38,7 +37,6 @@ type QMLer interface {
|
||||
ProcessFinished()
|
||||
NotifyHasNoKeychain()
|
||||
SetConnectionStatus(bool)
|
||||
SetIsRestarting(bool)
|
||||
SetAddAccountWarning(string, int)
|
||||
NotifyBubble(int, string)
|
||||
EmitEvent(string, string)
|
||||
@ -50,23 +48,25 @@ type QMLer interface {
|
||||
|
||||
// Accounts holds functionality of users
|
||||
type Accounts struct {
|
||||
Model *AccountsModel
|
||||
qml QMLer
|
||||
um types.UserManager
|
||||
prefs *config.Preferences
|
||||
Model *AccountsModel
|
||||
qml QMLer
|
||||
um types.UserManager
|
||||
settings *settings.Settings
|
||||
|
||||
authClient pmapi.Client
|
||||
auth *pmapi.Auth
|
||||
|
||||
LatestUserID string
|
||||
accountMutex sync.Mutex
|
||||
restarter types.Restarter
|
||||
}
|
||||
|
||||
// SetupAccounts will create Model and set QMLer and UserManager
|
||||
func (a *Accounts) SetupAccounts(qml QMLer, um types.UserManager) {
|
||||
func (a *Accounts) SetupAccounts(qml QMLer, um types.UserManager, restarter types.Restarter) {
|
||||
a.Model = NewAccountsModel(nil)
|
||||
a.qml = qml
|
||||
a.um = um
|
||||
a.restarter = restarter
|
||||
}
|
||||
|
||||
// LoadAccounts refreshes the current account list in GUI
|
||||
@ -102,9 +102,9 @@ func (a *Accounts) LoadAccounts() {
|
||||
accInfo.SetUserID(user.ID())
|
||||
accInfo.SetHostname(bridge.Host)
|
||||
accInfo.SetPassword(user.GetBridgePassword())
|
||||
if a.prefs != nil {
|
||||
accInfo.SetPortIMAP(a.prefs.GetInt(preferences.IMAPPortKey))
|
||||
accInfo.SetPortSMTP(a.prefs.GetInt(preferences.SMTPPortKey))
|
||||
if a.settings != nil {
|
||||
accInfo.SetPortIMAP(a.settings.GetInt(settings.IMAPPortKey))
|
||||
accInfo.SetPortSMTP(a.settings.GetInt(settings.SMTPPortKey))
|
||||
}
|
||||
|
||||
// Set aliases.
|
||||
@ -127,7 +127,7 @@ func (a *Accounts) ClearCache() {
|
||||
}
|
||||
// Clearing data removes everything (db, preferences, ...)
|
||||
// so everything has to be stopped and started again.
|
||||
a.qml.SetIsRestarting(true)
|
||||
a.restarter.SetToRestart()
|
||||
a.qml.Quit()
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ func (a *Accounts) ClearKeychain() {
|
||||
for _, user := range a.um.GetUsers() {
|
||||
if err := a.um.DeleteUser(user.ID(), false); err != nil {
|
||||
log.Error("While deleting user: ", err)
|
||||
if err == keychain.ErrNoKeychainInstalled { // Probably not needed anymore.
|
||||
if err == keychain.ErrNoKeychain { // Probably not needed anymore.
|
||||
a.qml.NotifyHasNoKeychain()
|
||||
}
|
||||
}
|
||||
@ -172,7 +172,6 @@ func (a *Accounts) showLoginError(err error, scope string) bool {
|
||||
}
|
||||
a.qml.SetConnectionStatus(true) // If we are here connection is ok.
|
||||
if err == pmapi.ErrUpgradeApplication {
|
||||
a.qml.EmitEvent(events.UpgradeApplicationEvent, "")
|
||||
return true
|
||||
}
|
||||
a.qml.SetAddAccountWarning(err.Error(), -1)
|
||||
@ -249,7 +248,7 @@ func (a *Accounts) DeleteAccount(iAccount int, removePreferences bool) {
|
||||
userID := a.Model.get(iAccount).UserID()
|
||||
if err := a.um.DeleteUser(userID, removePreferences); err != nil {
|
||||
log.Warn("deleteUser: cannot remove user: ", err)
|
||||
if err == keychain.ErrNoKeychainInstalled {
|
||||
if err == keychain.ErrNoKeychain {
|
||||
a.qml.NotifyHasNoKeychain()
|
||||
return
|
||||
}
|
||||
|
||||
@ -111,10 +111,12 @@ func WaitForEnter() {
|
||||
|
||||
type Listener interface {
|
||||
Add(string, chan<- string)
|
||||
RetryEmit(string)
|
||||
}
|
||||
|
||||
func MakeAndRegisterEvent(eventListener Listener, event string) <-chan string {
|
||||
ch := make(chan string)
|
||||
eventListener.Add(event, ch)
|
||||
eventListener.RetryEmit(event)
|
||||
return ch
|
||||
}
|
||||
|
||||
@ -22,16 +22,15 @@ package qtie
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
|
||||
"github.com/therecipe/qt/core"
|
||||
@ -51,9 +50,10 @@ var log = logrus.WithField("pkg", "frontend-qt-ie")
|
||||
// Qt and QML objects. QML signals and slots are connected via methods of GoQMLInterface.
|
||||
type FrontendQt struct {
|
||||
panicHandler types.PanicHandler
|
||||
config *config.Config
|
||||
locations *locations.Locations
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
updates types.Updater
|
||||
updater types.Updater
|
||||
ie types.ImportExporter
|
||||
|
||||
App *widgets.QApplication // Main Application pointer
|
||||
@ -62,7 +62,7 @@ type FrontendQt struct {
|
||||
Qml *GoQMLInterface // Object accessible from both Go and QML for methods and signals
|
||||
Accounts qtcommon.Accounts // Providing data for accounts ListView
|
||||
|
||||
programName string // Program name
|
||||
programName string // App name
|
||||
programVersion string // Program version
|
||||
buildVersion string // Program build version
|
||||
|
||||
@ -72,44 +72,43 @@ type FrontendQt struct {
|
||||
transfer *transfer.Transfer
|
||||
progress *transfer.Progress
|
||||
|
||||
notifyHasNoKeychain bool
|
||||
restarter types.Restarter
|
||||
|
||||
// saving most up-to-date update info to install it manually
|
||||
updateInfo updater.VersionInfo
|
||||
}
|
||||
|
||||
// New is constructor for Import-Export Qt-Go interface
|
||||
func New(
|
||||
version, buildVersion string,
|
||||
version, buildVersion, programName string,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
ie types.ImportExporter,
|
||||
restarter types.Restarter,
|
||||
) *FrontendQt {
|
||||
f := &FrontendQt{
|
||||
panicHandler: panicHandler,
|
||||
config: config,
|
||||
programName: "ProtonMail Import-Export",
|
||||
locations: locations,
|
||||
settings: settings,
|
||||
programName: programName,
|
||||
programVersion: "v" + version,
|
||||
eventListener: eventListener,
|
||||
updater: updater,
|
||||
buildVersion: buildVersion,
|
||||
updates: updates,
|
||||
ie: ie,
|
||||
restarter: restarter,
|
||||
}
|
||||
|
||||
log.Debugf("New Qt frontend: %p", f)
|
||||
return f
|
||||
}
|
||||
|
||||
// IsAppRestarting for Import-Export is always false i.e never restarts
|
||||
func (f *FrontendQt) IsAppRestarting() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Loop function for Import-Export interface. It runs QtExecute in main thread
|
||||
// with no additional function.
|
||||
func (f *FrontendQt) Loop(setupError error) (err error) {
|
||||
if setupError != nil {
|
||||
f.notifyHasNoKeychain = true
|
||||
}
|
||||
func (f *FrontendQt) Loop() (err error) {
|
||||
go func() {
|
||||
defer f.panicHandler.HandlePanic()
|
||||
f.watchEvents()
|
||||
@ -118,9 +117,32 @@ func (f *FrontendQt) Loop(setupError error) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *FrontendQt) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
|
||||
f.SetVersion(update)
|
||||
f.Qml.SetUpdateCanInstall(canInstall)
|
||||
f.Qml.NotifyManualUpdate()
|
||||
}
|
||||
|
||||
func (f *FrontendQt) SetVersion(version updater.VersionInfo) {
|
||||
f.Qml.SetUpdateVersion(version.Version.String())
|
||||
f.Qml.SetUpdateLandingPage(version.LandingPage)
|
||||
f.Qml.SetUpdateReleaseNotesLink(version.ReleaseNotesPage)
|
||||
f.updateInfo = version
|
||||
}
|
||||
|
||||
func (f *FrontendQt) NotifySilentUpdateInstalled() {
|
||||
f.Qml.NotifySilentUpdateRestartNeeded()
|
||||
}
|
||||
|
||||
func (f *FrontendQt) NotifySilentUpdateError(err error) {
|
||||
f.Qml.NotifySilentUpdateError()
|
||||
}
|
||||
|
||||
func (f *FrontendQt) watchEvents() {
|
||||
credentialsErrorCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.CredentialsErrorEvent)
|
||||
internetOffCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOffEvent)
|
||||
internetOnCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOnEvent)
|
||||
secondInstanceCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.SecondInstanceEvent)
|
||||
restartBridgeCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.RestartBridgeEvent)
|
||||
addressChangedCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedLogoutEvent)
|
||||
@ -129,12 +151,16 @@ func (f *FrontendQt) watchEvents() {
|
||||
newUserCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UserRefreshEvent)
|
||||
for {
|
||||
select {
|
||||
case <-credentialsErrorCh:
|
||||
f.Qml.NotifyHasNoKeychain()
|
||||
case <-internetOffCh:
|
||||
f.Qml.SetConnectionStatus(false)
|
||||
case <-internetOnCh:
|
||||
f.Qml.SetConnectionStatus(true)
|
||||
case <-secondInstanceCh:
|
||||
f.Qml.ShowWindow()
|
||||
case <-restartBridgeCh:
|
||||
f.Qml.SetIsRestarting(true)
|
||||
f.restarter.SetToRestart()
|
||||
f.App.Quit()
|
||||
case address := <-addressChangedCh:
|
||||
f.Qml.NotifyAddressChanged(address)
|
||||
@ -148,7 +174,7 @@ func (f *FrontendQt) watchEvents() {
|
||||
f.Qml.NotifyLogout(user.Username())
|
||||
case <-updateApplicationCh:
|
||||
f.Qml.ProcessFinished()
|
||||
f.Qml.NotifyUpdate()
|
||||
f.Qml.NotifyForceUpdate()
|
||||
case <-newUserCh:
|
||||
f.Qml.LoadAccounts()
|
||||
}
|
||||
@ -165,7 +191,7 @@ func (f *FrontendQt) qtSetupQmlAndStructures() {
|
||||
f.View.RootContext().SetContextProperty("go", f.Qml)
|
||||
|
||||
// Add AccountsModel
|
||||
f.Accounts.SetupAccounts(f.Qml, f.ie)
|
||||
f.Accounts.SetupAccounts(f.Qml, f.ie, f.restarter)
|
||||
f.View.RootContext().SetContextProperty("accountsModel", f.Accounts.Model)
|
||||
|
||||
// Add TransferRules structure
|
||||
@ -189,11 +215,6 @@ func (f *FrontendQt) qtSetupQmlAndStructures() {
|
||||
} else {
|
||||
f.Qml.SetIsFirstStart(false)
|
||||
}
|
||||
|
||||
// Notify user about error during initialization.
|
||||
if f.notifyHasNoKeychain {
|
||||
f.Qml.NotifyHasNoKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
// QtExecute in main for starting Qt application
|
||||
@ -220,6 +241,12 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
|
||||
f.Qml.SetCredits(importexport.Credits)
|
||||
f.Qml.SetFullversion(f.buildVersion)
|
||||
|
||||
if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||
f.Qml.SetIsAutoUpdate(true)
|
||||
} else {
|
||||
f.Qml.SetIsAutoUpdate(false)
|
||||
}
|
||||
|
||||
// Loop
|
||||
if ret := gui.QGuiApplication_Exec(); ret != 0 {
|
||||
//err := errors.New(errors.ErrQApplication, "Event loop ended with return value: %v", string(ret))
|
||||
@ -233,7 +260,12 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
|
||||
}
|
||||
|
||||
func (f *FrontendQt) openLogs() {
|
||||
go open.Run(f.config.GetLogDir())
|
||||
logsPath, err := f.locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go open.Run(logsPath)
|
||||
}
|
||||
|
||||
func (f *FrontendQt) openReport() {
|
||||
@ -241,7 +273,7 @@ func (f *FrontendQt) openReport() {
|
||||
}
|
||||
|
||||
func (f *FrontendQt) openDownloadLink() {
|
||||
go open.Run(f.updates.GetDownloadLink())
|
||||
// NOTE: Fix this.
|
||||
}
|
||||
|
||||
// sendImportReport sends an anonymized import or export report file to our customer support
|
||||
@ -279,6 +311,9 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
|
||||
if f.Accounts.Model.Count() > 0 {
|
||||
accname = f.Accounts.Model.Get(0).Account()
|
||||
}
|
||||
if accname == "" {
|
||||
accname = "Unknown account"
|
||||
}
|
||||
|
||||
if err := f.ie.ReportBug(
|
||||
core.QSysInfo_ProductType(),
|
||||
@ -295,6 +330,18 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *FrontendQt) toggleAutoUpdate() {
|
||||
defer f.Qml.ProcessFinished()
|
||||
|
||||
if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||
f.settings.SetBool(settings.AutoUpdateKey, false)
|
||||
f.Qml.SetIsAutoUpdate(false)
|
||||
} else {
|
||||
f.settings.SetBool(settings.AutoUpdateKey, true)
|
||||
f.Qml.SetIsAutoUpdate(true)
|
||||
}
|
||||
}
|
||||
|
||||
// checkInternet is almost idetical to bridge
|
||||
func (f *FrontendQt) checkInternet() {
|
||||
f.Qml.SetConnectionStatus(f.ie.CheckConnection() == nil)
|
||||
@ -365,65 +412,57 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
|
||||
}()
|
||||
}
|
||||
|
||||
// StartUpdate is identical to bridge
|
||||
func (f *FrontendQt) StartUpdate() {
|
||||
progress := make(chan updates.Progress)
|
||||
go func() { // Update progress in QML.
|
||||
defer f.panicHandler.HandlePanic()
|
||||
for current := range progress {
|
||||
f.Qml.SetProgress(current.Processed)
|
||||
f.Qml.SetProgressDescription(strconv.Itoa(current.Description))
|
||||
// Error happend
|
||||
if current.Err != nil {
|
||||
log.Error("update progress: ", current.Err)
|
||||
f.Qml.UpdateFinished(true)
|
||||
return
|
||||
}
|
||||
// Finished everything OK.
|
||||
if current.Description >= updates.InfoQuitApp {
|
||||
f.Qml.UpdateFinished(false)
|
||||
time.Sleep(3 * time.Second) // Just notify.
|
||||
f.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp)
|
||||
f.App.Quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
func (f *FrontendQt) startManualUpdate() {
|
||||
go func() {
|
||||
defer f.panicHandler.HandlePanic()
|
||||
f.updates.StartUpgrade(progress)
|
||||
err := f.updater.InstallUpdate(f.updateInfo)
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("An error occurred while installing updates manually")
|
||||
f.Qml.NotifyManualUpdateError()
|
||||
} else {
|
||||
f.Qml.NotifyManualUpdateRestartNeeded()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// isNewVersionAvailable is identical to bridge
|
||||
// return 0 when local version is fine
|
||||
// return 1 when new version is available
|
||||
func (f *FrontendQt) isNewVersionAvailable(showMessage bool) {
|
||||
func (f *FrontendQt) checkForUpdatesAndWait() {
|
||||
version, err := f.updater.Check()
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("An error occurred while checking updates manually")
|
||||
f.Qml.NotifyManualUpdateError()
|
||||
return
|
||||
}
|
||||
|
||||
f.SetVersion(version)
|
||||
|
||||
if !f.updater.IsUpdateApplicable(version) {
|
||||
logrus.Debug("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("version", version.Version).Info("An update is available")
|
||||
|
||||
if !f.updater.CanInstall(version) {
|
||||
logrus.Debug("A manual update is required")
|
||||
f.NotifyManualUpdate(version, false)
|
||||
return
|
||||
}
|
||||
|
||||
f.NotifyManualUpdate(version, true)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) checkAndOpenReleaseNotes() {
|
||||
go func() {
|
||||
defer f.Qml.ProcessFinished()
|
||||
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
|
||||
if err != nil {
|
||||
log.Warnln("Cannot retrieve version info: ", err)
|
||||
f.checkInternet()
|
||||
return
|
||||
}
|
||||
f.Qml.SetConnectionStatus(true) // if we are here connection is ok
|
||||
if isUpToDate {
|
||||
f.Qml.SetUpdateState(StatusUpToDate)
|
||||
if showMessage {
|
||||
f.Qml.NotifyVersionIsTheLatest()
|
||||
}
|
||||
return
|
||||
}
|
||||
f.Qml.SetNewversion(latestVersionInfo.Version)
|
||||
f.Qml.SetChangelog(latestVersionInfo.ReleaseNotes)
|
||||
f.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs)
|
||||
f.Qml.SetLandingPage(latestVersionInfo.LandingPage)
|
||||
f.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink())
|
||||
f.Qml.SetUpdateState(StatusNewVersionAvailable)
|
||||
s.checkForUpdatesAndWait()
|
||||
s.Qml.OpenReleaseNotesExternally()
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) checkForUpdates() {
|
||||
go s.checkForUpdatesAndWait()
|
||||
}
|
||||
|
||||
func (f *FrontendQt) resetSource() {
|
||||
if f.transfer != nil {
|
||||
f.transfer.ResetRules()
|
||||
@ -434,16 +473,12 @@ func (f *FrontendQt) resetSource() {
|
||||
}
|
||||
|
||||
func (f *FrontendQt) openLicenseFile() {
|
||||
go open.Run(f.config.GetLicenseFilePath())
|
||||
go open.Run(f.locations.GetLicenseFilePath())
|
||||
}
|
||||
|
||||
// getLocalVersionInfo is identical to bridge.
|
||||
func (f *FrontendQt) getLocalVersionInfo() {
|
||||
defer f.Qml.ProcessFinished()
|
||||
localVersion := f.updates.GetLocalVersion()
|
||||
f.Qml.SetNewversion(localVersion.Version)
|
||||
f.Qml.SetChangelog(localVersion.ReleaseNotes)
|
||||
f.Qml.SetBugfixes(localVersion.ReleaseFixedBugs)
|
||||
// NOTE: Fix this.
|
||||
}
|
||||
|
||||
// LeastUsedColor is intended to return color for creating a new inbox or label.
|
||||
|
||||
@ -23,8 +23,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -33,23 +35,36 @@ var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
|
||||
|
||||
type FrontendHeadless struct{}
|
||||
|
||||
func (s *FrontendHeadless) Loop(credentialsError error) error {
|
||||
log.Info("Check status on localhost:8081")
|
||||
func (s *FrontendHeadless) Loop() error {
|
||||
log.Info("Check status on localhost:8082")
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "IE is running")
|
||||
})
|
||||
return http.ListenAndServe(":8081", nil)
|
||||
return http.ListenAndServe(":8082", nil)
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) IsAppRestarting() bool { return false }
|
||||
func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
|
||||
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) SetVersion(update updater.VersionInfo) {
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) NotifySilentUpdateInstalled() {
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) NotifySilentUpdateError(err error) {
|
||||
}
|
||||
|
||||
func New(
|
||||
version, buildVersion string,
|
||||
version, buildVersion, appName string,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
ie types.ImportExporter,
|
||||
restarter types.Restarter,
|
||||
) *FrontendHeadless {
|
||||
return &FrontendHeadless{}
|
||||
}
|
||||
|
||||
@ -33,11 +33,11 @@ type GoQMLInterface struct {
|
||||
|
||||
_ func() `constructor:"init"`
|
||||
|
||||
_ bool `property:"isAutoUpdate"`
|
||||
_ string `property:"currentAddress"`
|
||||
_ string `property:"goos"`
|
||||
_ string `property:"credits"`
|
||||
_ bool `property:"isFirstStart"`
|
||||
_ bool `property:"isRestarting"`
|
||||
_ bool `property:"isConnectionOK"`
|
||||
|
||||
_ string `property:lastError`
|
||||
@ -50,12 +50,23 @@ type GoQMLInterface struct {
|
||||
_ string `property:importLogFileName`
|
||||
|
||||
_ string `property:"programTitle"`
|
||||
_ string `property:"newversion"`
|
||||
_ string `property:"fullversion"`
|
||||
_ string `property:"downloadLink"`
|
||||
_ string `property:"landingPage"`
|
||||
_ string `property:"changelog"`
|
||||
_ string `property:"bugfixes"`
|
||||
|
||||
_ string `property:"updateVersion"`
|
||||
_ bool `property:"updateCanInstall"`
|
||||
_ string `property:"updateLandingPage"`
|
||||
_ string `property:"updateReleaseNotesLink"`
|
||||
_ func() `signal:"notifyManualUpdate"`
|
||||
_ func() `signal:"notifyManualUpdateRestartNeeded"`
|
||||
_ func() `signal:"notifyManualUpdateError"`
|
||||
_ func() `signal:"notifyForceUpdate"`
|
||||
_ func() `signal:"notifySilentUpdateRestartNeeded"`
|
||||
_ func() `signal:"notifySilentUpdateError"`
|
||||
_ func() `slot:"checkForUpdates"`
|
||||
_ func() `slot:"checkAndOpenReleaseNotes"`
|
||||
_ func() `signal:"openReleaseNotesExternally"`
|
||||
_ func() `slot:"startManualUpdate"`
|
||||
|
||||
// translations
|
||||
_ string `property:"wrongCredentials"`
|
||||
@ -68,6 +79,8 @@ type GoQMLInterface struct {
|
||||
_ func(updateState string) `signal:"setUpdateState"`
|
||||
_ func() `slot:"checkInternet"`
|
||||
|
||||
_ func() `slot:"setToRestart"`
|
||||
|
||||
_ func() `signal:"processFinished"`
|
||||
_ func(okay bool) `signal:"exportStructureLoadFinished"`
|
||||
_ func(okay bool) `signal:"importStructuresLoadFinished"`
|
||||
@ -77,6 +90,9 @@ type GoQMLInterface struct {
|
||||
_ func() `slot:"getLocalVersionInfo"`
|
||||
_ func() `slot:"loadImportReports"`
|
||||
|
||||
_ func() `signal:"showWindow"`
|
||||
|
||||
_ func() `slot:"toggleAutoUpdate"`
|
||||
_ func() `slot:"quit"`
|
||||
_ func() `slot:"loadAccounts"`
|
||||
_ func() `slot:"openLogs"`
|
||||
@ -87,8 +103,7 @@ type GoQMLInterface struct {
|
||||
_ func() `signal:"highlightSystray"`
|
||||
_ func() `signal:"normalSystray"`
|
||||
|
||||
_ func(showMessage bool) `slot:"isNewVersionAvailable"`
|
||||
_ func() string `slot:"getBackendVersion"`
|
||||
_ func() string `slot:"getBackendVersion"`
|
||||
|
||||
_ func(description, client, address string) bool `slot:"sendBug"`
|
||||
_ func(address string) bool `slot:"sendImportReport"`
|
||||
@ -126,12 +141,10 @@ type GoQMLInterface struct {
|
||||
_ func() `signal:"notifyVersionIsTheLatest"`
|
||||
_ func() `signal:"notifyKeychainRebuild"`
|
||||
_ func() `signal:"notifyHasNoKeychain"`
|
||||
_ func() `signal:"notifyUpdate"`
|
||||
_ func(accname string) `signal:"notifyLogout"`
|
||||
_ func(accname string) `signal:"notifyAddressChanged"`
|
||||
_ func(accname string) `signal:"notifyAddressChangedLogout"`
|
||||
|
||||
_ func() `slot:"startUpdate"`
|
||||
_ func(hasError bool) `signal:"updateFinished"`
|
||||
|
||||
// errors
|
||||
@ -148,6 +161,7 @@ func (s *GoQMLInterface) init() {}
|
||||
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectQuit(f.App.Quit)
|
||||
|
||||
s.ConnectToggleAutoUpdate(f.toggleAutoUpdate)
|
||||
s.ConnectLoadAccounts(f.Accounts.LoadAccounts)
|
||||
s.ConnectOpenLogs(f.openLogs)
|
||||
s.ConnectOpenDownloadLink(f.openDownloadLink)
|
||||
@ -165,18 +179,19 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectAddAccount(f.Accounts.AddAccount)
|
||||
|
||||
s.SetGoos(runtime.GOOS)
|
||||
s.SetIsRestarting(false)
|
||||
s.SetProgramTitle(f.programName)
|
||||
|
||||
s.ConnectOpenLicenseFile(f.openLicenseFile)
|
||||
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
|
||||
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
|
||||
s.ConnectCheckForUpdates(f.checkForUpdates)
|
||||
s.ConnectGetBackendVersion(func() string {
|
||||
return f.programVersion
|
||||
})
|
||||
|
||||
s.ConnectCheckInternet(f.checkInternet)
|
||||
|
||||
s.ConnectSetToRestart(f.restarter.SetToRestart)
|
||||
|
||||
s.ConnectLoadStructureForExport(f.LoadStructureForExport)
|
||||
s.ConnectSetupAndLoadForImport(f.setupAndLoadForImport)
|
||||
s.ConnectResetSource(f.resetSource)
|
||||
@ -188,7 +203,5 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
|
||||
s.ConnectCheckPathStatus(CheckPathStatus)
|
||||
|
||||
s.ConnectStartUpdate(f.StartUpdate)
|
||||
|
||||
s.ConnectEmitEvent(f.emitEvent)
|
||||
}
|
||||
|
||||
@ -24,8 +24,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
@ -63,8 +63,8 @@ func (s *FrontendQt) loadAccounts() {
|
||||
acc_info.SetUserID(user.ID())
|
||||
acc_info.SetHostname(bridge.Host)
|
||||
acc_info.SetPassword(user.GetBridgePassword())
|
||||
acc_info.SetPortIMAP(s.preferences.GetInt(preferences.IMAPPortKey))
|
||||
acc_info.SetPortSMTP(s.preferences.GetInt(preferences.SMTPPortKey))
|
||||
acc_info.SetPortIMAP(s.settings.GetInt(settings.IMAPPortKey))
|
||||
acc_info.SetPortSMTP(s.settings.GetInt(settings.SMTPPortKey))
|
||||
|
||||
// Set aliases.
|
||||
acc_info.SetAliases(strings.Join(user.GetAddresses(), ";"))
|
||||
@ -85,7 +85,7 @@ func (s *FrontendQt) clearCache() {
|
||||
}
|
||||
// Clearing data removes everything (db, preferences, ...)
|
||||
// so everything has to be stopped and started again.
|
||||
s.Qml.SetIsRestarting(true)
|
||||
s.restarter.SetToRestart()
|
||||
s.App.Quit()
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ func (s *FrontendQt) clearKeychain() {
|
||||
for _, user := range s.bridge.GetUsers() {
|
||||
if err := s.bridge.DeleteUser(user.ID(), false); err != nil {
|
||||
log.Error("While deleting user: ", err)
|
||||
if err == keychain.ErrNoKeychainInstalled { // Probably not needed anymore.
|
||||
if err == keychain.ErrNoKeychain { // Probably not needed anymore.
|
||||
s.Qml.NotifyHasNoKeychain()
|
||||
}
|
||||
}
|
||||
@ -128,7 +128,6 @@ func (s *FrontendQt) showLoginError(err error, scope string) bool {
|
||||
}
|
||||
s.Qml.SetConnectionStatus(true) // If we are here connection is ok.
|
||||
if err == pmapi.ErrUpgradeApplication {
|
||||
s.eventListener.Emit(events.UpgradeApplicationEvent, "")
|
||||
return true
|
||||
}
|
||||
s.Qml.SetAddAccountWarning(err.Error(), -1)
|
||||
@ -203,7 +202,7 @@ func (s *FrontendQt) deleteAccount(iAccount int, removePreferences bool) {
|
||||
userID := s.Accounts.get(iAccount).UserID()
|
||||
if err := s.bridge.DeleteUser(userID, removePreferences); err != nil {
|
||||
log.Warn("deleteUser: cannot remove user: ", err)
|
||||
if err == keychain.ErrNoKeychainInstalled {
|
||||
if err == keychain.ErrNoKeychain {
|
||||
s.Qml.NotifyHasNoKeychain()
|
||||
return
|
||||
}
|
||||
|
||||
@ -38,18 +38,17 @@ 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/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"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/kardianos/osext"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/therecipe/qt/core"
|
||||
@ -68,76 +67,69 @@ var accountMutex = &sync.Mutex{}
|
||||
type FrontendQt struct {
|
||||
version string
|
||||
buildVersion string
|
||||
programName string
|
||||
showWindowOnStart bool
|
||||
panicHandler types.PanicHandler
|
||||
config *config.Config
|
||||
preferences *config.Preferences
|
||||
locations *locations.Locations
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
updates types.Updater
|
||||
updater types.Updater
|
||||
bridge types.Bridger
|
||||
noEncConfirmator types.NoEncConfirmator
|
||||
|
||||
App *widgets.QApplication // Main Application pointer.
|
||||
View *qml.QQmlApplicationEngine // QML engine pointer.
|
||||
MainWin *core.QObject // Pointer to main window inside QML.
|
||||
Qml *GoQMLInterface // Object accessible from both Go and QML for methods and signals.
|
||||
Accounts *AccountsModel // Providing data for accounts ListView.
|
||||
programName string // Program name (shown in taskbar).
|
||||
programVer string // Program version (shown in help).
|
||||
App *widgets.QApplication // Main Application pointer.
|
||||
View *qml.QQmlApplicationEngine // QML engine pointer.
|
||||
MainWin *core.QObject // Pointer to main window inside QML.
|
||||
Qml *GoQMLInterface // Object accessible from both Go and QML for methods and signals.
|
||||
Accounts *AccountsModel // Providing data for accounts ListView.
|
||||
programVer string // Program version (shown in help).
|
||||
|
||||
authClient pmapi.Client
|
||||
|
||||
auth *pmapi.Auth
|
||||
|
||||
AutostartEntry *autostart.App
|
||||
autostart *autostart.App
|
||||
|
||||
// expand userID when added
|
||||
userIDAdded string
|
||||
|
||||
notifyHasNoKeychain bool
|
||||
restarter types.Restarter
|
||||
|
||||
// saving most up-to-date update info to install it manually
|
||||
updateInfo updater.VersionInfo
|
||||
}
|
||||
|
||||
// New returns a new Qt frontendend for the bridge.
|
||||
// New returns a new Qt frontend for the bridge.
|
||||
func New(
|
||||
version,
|
||||
buildVersion string,
|
||||
buildVersion,
|
||||
programName string,
|
||||
showWindowOnStart bool,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
preferences *config.Preferences,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
restarter types.Restarter,
|
||||
) *FrontendQt {
|
||||
prgName := "ProtonMail Bridge"
|
||||
tmp := &FrontendQt{
|
||||
version: version,
|
||||
buildVersion: buildVersion,
|
||||
programName: programName,
|
||||
showWindowOnStart: showWindowOnStart,
|
||||
panicHandler: panicHandler,
|
||||
config: config,
|
||||
preferences: preferences,
|
||||
locations: locations,
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
updates: updates,
|
||||
updater: updater,
|
||||
bridge: bridge,
|
||||
noEncConfirmator: noEncConfirmator,
|
||||
|
||||
programName: prgName,
|
||||
programVer: "v" + version,
|
||||
AutostartEntry: &autostart.App{
|
||||
Name: prgName,
|
||||
DisplayName: prgName,
|
||||
Exec: []string{"", "--no-window"},
|
||||
},
|
||||
}
|
||||
|
||||
// Handle autostart if wanted.
|
||||
if p, err := osext.Executable(); err == nil {
|
||||
tmp.AutostartEntry.Exec[0] = p
|
||||
log.Info("Autostart ", p)
|
||||
} else {
|
||||
log.Error("Cannot get current executable path: ", err)
|
||||
programVer: "v" + version,
|
||||
autostart: autostart,
|
||||
restarter: restarter,
|
||||
}
|
||||
|
||||
// Nicer string for OS.
|
||||
@ -161,10 +153,7 @@ func (s *FrontendQt) InstanceExistAlert() {
|
||||
// Loop function for Bridge interface.
|
||||
//
|
||||
// It runs QtExecute in main thread with no additional function.
|
||||
func (s *FrontendQt) Loop(credentialsError error) (err error) {
|
||||
if credentialsError != nil {
|
||||
s.notifyHasNoKeychain = true
|
||||
}
|
||||
func (s *FrontendQt) Loop() (err error) {
|
||||
go func() {
|
||||
defer s.panicHandler.HandlePanic()
|
||||
s.watchEvents()
|
||||
@ -173,8 +162,30 @@ func (s *FrontendQt) Loop(credentialsError error) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *FrontendQt) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
|
||||
s.SetVersion(update)
|
||||
s.Qml.SetUpdateCanInstall(canInstall)
|
||||
s.Qml.NotifyManualUpdate()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) SetVersion(version updater.VersionInfo) {
|
||||
s.Qml.SetUpdateVersion(version.Version.String())
|
||||
s.Qml.SetUpdateLandingPage(version.LandingPage)
|
||||
s.Qml.SetUpdateReleaseNotesLink(version.ReleaseNotesPage)
|
||||
s.updateInfo = version
|
||||
}
|
||||
|
||||
func (s *FrontendQt) NotifySilentUpdateInstalled() {
|
||||
s.Qml.NotifySilentUpdateRestartNeeded()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) NotifySilentUpdateError(err error) {
|
||||
s.Qml.NotifySilentUpdateError()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) watchEvents() {
|
||||
errorCh := s.getEventChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := s.getEventChannel(events.CredentialsErrorEvent)
|
||||
outgoingNoEncCh := s.getEventChannel(events.OutgoingNoEncEvent)
|
||||
noActiveKeyForRecipientCh := s.getEventChannel(events.NoActiveKeyForRecipientEvent)
|
||||
internetOffCh := s.getEventChannel(events.InternetOffEvent)
|
||||
@ -193,6 +204,8 @@ func (s *FrontendQt) watchEvents() {
|
||||
imapIssue := strings.Contains(errorDetails, "IMAP failed")
|
||||
smtpIssue := strings.Contains(errorDetails, "SMTP failed")
|
||||
s.Qml.NotifyPortIssue(imapIssue, smtpIssue)
|
||||
case <-credentialsErrorCh:
|
||||
s.Qml.NotifyHasNoKeychain()
|
||||
case idAndSubject := <-outgoingNoEncCh:
|
||||
idAndSubjectSlice := strings.SplitN(idAndSubject, ":", 2)
|
||||
messageID := idAndSubjectSlice[0]
|
||||
@ -207,7 +220,7 @@ func (s *FrontendQt) watchEvents() {
|
||||
case <-secondInstanceCh:
|
||||
s.Qml.ShowWindow()
|
||||
case <-restartBridgeCh:
|
||||
s.Qml.SetIsRestarting(true)
|
||||
s.restarter.SetToRestart()
|
||||
// watchEvents is started in parallel with the Qt app.
|
||||
// If the event comes too early, app might not be ready yet.
|
||||
if s.App != nil {
|
||||
@ -225,7 +238,7 @@ func (s *FrontendQt) watchEvents() {
|
||||
s.Qml.NotifyLogout(user.Username())
|
||||
case <-updateApplicationCh:
|
||||
s.Qml.ProcessFinished()
|
||||
s.Qml.NotifyUpdate()
|
||||
s.Qml.NotifyForceUpdate()
|
||||
case <-newUserCh:
|
||||
s.Qml.LoadAccounts()
|
||||
case <-certIssue:
|
||||
@ -267,10 +280,6 @@ func (s *FrontendQt) Start() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FrontendQt) IsAppRestarting() bool {
|
||||
return s.Qml.IsRestarting()
|
||||
}
|
||||
|
||||
// InvMethod runs the function with name `method` defined in RootObject of the QML.
|
||||
// Used for tests.
|
||||
func (s *FrontendQt) InvMethod(method string) error {
|
||||
@ -304,13 +313,13 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
|
||||
s.View.RootContext().SetContextProperty("go", s.Qml)
|
||||
|
||||
// Set first start flag.
|
||||
s.Qml.SetIsFirstStart(s.preferences.GetBool(preferences.FirstStartGUIKey))
|
||||
s.preferences.SetBool(preferences.FirstStartGUIKey, false)
|
||||
s.Qml.SetIsFirstStart(s.settings.GetBool(settings.FirstStartGUIKey))
|
||||
s.settings.SetBool(settings.FirstStartGUIKey, false)
|
||||
|
||||
// Check if it is first start after update (fresh version).
|
||||
lastVersion := s.preferences.Get(preferences.LastVersionKey)
|
||||
lastVersion := s.settings.Get(settings.LastVersionKey)
|
||||
s.Qml.SetIsFreshVersion(lastVersion != "" && s.version != lastVersion)
|
||||
s.preferences.Set(preferences.LastVersionKey, s.version)
|
||||
s.settings.Set(settings.LastVersionKey, s.version)
|
||||
|
||||
// Add AccountsModel.
|
||||
s.Accounts = NewAccountsModel(nil)
|
||||
@ -325,41 +334,51 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
|
||||
|
||||
// Autostart.
|
||||
if s.Qml.IsFirstStart() {
|
||||
if s.AutostartEntry.IsEnabled() {
|
||||
if err := s.AutostartEntry.Disable(); err != nil {
|
||||
if s.autostart.IsEnabled() {
|
||||
if err := s.autostart.Disable(); err != nil {
|
||||
log.Error("First disable ", err)
|
||||
s.autostartError(err)
|
||||
}
|
||||
}
|
||||
s.toggleAutoStart()
|
||||
}
|
||||
if s.AutostartEntry.IsEnabled() {
|
||||
if s.autostart.IsEnabled() {
|
||||
s.Qml.SetIsAutoStart(true)
|
||||
} else {
|
||||
s.Qml.SetIsAutoStart(false)
|
||||
}
|
||||
|
||||
if s.preferences.GetBool(preferences.AllowProxyKey) {
|
||||
if s.settings.GetBool(settings.AutoUpdateKey) {
|
||||
s.Qml.SetIsAutoUpdate(true)
|
||||
} else {
|
||||
s.Qml.SetIsAutoUpdate(false)
|
||||
}
|
||||
|
||||
if s.settings.GetBool(settings.AllowProxyKey) {
|
||||
s.Qml.SetIsProxyAllowed(true)
|
||||
} else {
|
||||
s.Qml.SetIsProxyAllowed(false)
|
||||
}
|
||||
|
||||
// Notify user about error during initialization.
|
||||
if s.notifyHasNoKeychain {
|
||||
s.Qml.NotifyHasNoKeychain()
|
||||
if updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel {
|
||||
s.Qml.SetIsEarlyAccess(true)
|
||||
} else {
|
||||
s.Qml.SetIsEarlyAccess(false)
|
||||
}
|
||||
|
||||
s.eventListener.RetryEmit(events.TLSCertIssue)
|
||||
s.eventListener.RetryEmit(events.ErrorEvent)
|
||||
|
||||
// Set reporting of outgoing email without encryption.
|
||||
s.Qml.SetIsReportingOutgoingNoEnc(s.preferences.GetBool(preferences.ReportOutgoingNoEncKey))
|
||||
s.Qml.SetIsReportingOutgoingNoEnc(s.settings.GetBool(settings.ReportOutgoingNoEncKey))
|
||||
|
||||
defaultIMAPPort, _ := strconv.Atoi(settings.DefaultIMAPPort)
|
||||
defaultSMTPPort, _ := strconv.Atoi(settings.DefaultSMTPPort)
|
||||
|
||||
// IMAP/SMTP ports.
|
||||
s.Qml.SetIsDefaultPort(
|
||||
s.config.GetDefaultIMAPPort() == s.preferences.GetInt(preferences.IMAPPortKey) &&
|
||||
s.config.GetDefaultSMTPPort() == s.preferences.GetInt(preferences.SMTPPortKey),
|
||||
defaultIMAPPort == s.settings.GetInt(settings.IMAPPortKey) &&
|
||||
defaultSMTPPort == s.settings.GetInt(settings.SMTPPortKey),
|
||||
)
|
||||
|
||||
// Check QML is loaded properly.
|
||||
@ -387,48 +406,58 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
|
||||
}
|
||||
|
||||
func (s *FrontendQt) openLogs() {
|
||||
go open.Run(s.config.GetLogDir())
|
||||
logsPath, err := s.locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go open.Run(logsPath)
|
||||
}
|
||||
|
||||
// Check version in separate goroutine to not block the GUI (avoid program not responding message).
|
||||
func (s *FrontendQt) isNewVersionAvailable(showMessage bool) {
|
||||
func (s *FrontendQt) checkForUpdatesAndWait() {
|
||||
version, err := s.updater.Check()
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("An error occurred while checking updates manually")
|
||||
s.Qml.NotifyManualUpdateError()
|
||||
return
|
||||
}
|
||||
|
||||
s.SetVersion(version)
|
||||
|
||||
if !s.updater.IsUpdateApplicable(version) {
|
||||
logrus.Debug("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("version", version.Version).Info("An update is available")
|
||||
|
||||
if !s.updater.CanInstall(version) {
|
||||
logrus.Debug("A manual update is required")
|
||||
s.NotifyManualUpdate(version, false)
|
||||
return
|
||||
}
|
||||
|
||||
s.NotifyManualUpdate(version, true)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) checkAndOpenReleaseNotes() {
|
||||
go func() {
|
||||
defer s.panicHandler.HandlePanic()
|
||||
defer s.Qml.ProcessFinished()
|
||||
isUpToDate, latestVersionInfo, err := s.updates.CheckIsUpToDate()
|
||||
if err != nil {
|
||||
log.Warn("Can not retrieve version info: ", err)
|
||||
s.checkInternet()
|
||||
return
|
||||
}
|
||||
s.Qml.SetConnectionStatus(true) // If we are here connection is ok.
|
||||
if isUpToDate {
|
||||
s.Qml.SetUpdateState("upToDate")
|
||||
if showMessage {
|
||||
s.Qml.NotifyVersionIsTheLatest()
|
||||
}
|
||||
return
|
||||
}
|
||||
s.Qml.SetNewversion(latestVersionInfo.Version)
|
||||
s.Qml.SetChangelog(latestVersionInfo.ReleaseNotes)
|
||||
s.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs)
|
||||
s.Qml.SetLandingPage(latestVersionInfo.LandingPage)
|
||||
s.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink())
|
||||
s.Qml.ShowWindow()
|
||||
s.Qml.SetUpdateState("oldVersion")
|
||||
s.checkForUpdatesAndWait()
|
||||
s.Qml.OpenReleaseNotesExternally()
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) checkForUpdates() {
|
||||
go s.checkForUpdatesAndWait()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) openLicenseFile() {
|
||||
go open.Run(s.config.GetLicenseFilePath())
|
||||
go open.Run(s.locations.GetLicenseFilePath())
|
||||
}
|
||||
|
||||
func (s *FrontendQt) getLocalVersionInfo() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
localVersion := s.updates.GetLocalVersion()
|
||||
s.Qml.SetNewversion(localVersion.Version)
|
||||
s.Qml.SetChangelog(localVersion.ReleaseNotes)
|
||||
s.Qml.SetBugfixes(localVersion.ReleaseFixedBugs)
|
||||
// NOTE: Fix this.
|
||||
}
|
||||
|
||||
func (s *FrontendQt) sendBug(description, client, address string) (isOK bool) {
|
||||
@ -437,6 +466,9 @@ func (s *FrontendQt) sendBug(description, client, address string) (isOK bool) {
|
||||
if s.Accounts.Count() > 0 {
|
||||
accname = s.Accounts.get(0).Account()
|
||||
}
|
||||
if accname == "" {
|
||||
accname = "Unknown account"
|
||||
}
|
||||
if err := s.bridge.ReportBug(
|
||||
core.QSysInfo_ProductType(),
|
||||
core.QSysInfo_PrettyProductName(),
|
||||
@ -465,18 +497,22 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
|
||||
return
|
||||
}
|
||||
|
||||
imapPort := s.preferences.GetInt(preferences.IMAPPortKey)
|
||||
imapPort := s.settings.GetInt(settings.IMAPPortKey)
|
||||
imapSSL := false
|
||||
smtpPort := s.preferences.GetInt(preferences.SMTPPortKey)
|
||||
smtpSSL := s.preferences.GetBool(preferences.SMTPSSLKey)
|
||||
smtpPort := s.settings.GetInt(settings.SMTPPortKey)
|
||||
smtpSSL := s.settings.GetBool(settings.SMTPSSLKey)
|
||||
|
||||
// If configuring apple mail for Catalina or newer, users should use SSL.
|
||||
doRestart := false
|
||||
if !smtpSSL && useragent.IsCatalinaOrNewer() {
|
||||
smtpSSL = true
|
||||
s.preferences.SetBool(preferences.SMTPSSLKey, true)
|
||||
s.settings.SetBool(settings.SMTPSSLKey, true)
|
||||
log.Warn("Detected Catalina or newer with bad SMTP SSL settings, now using SSL, bridge needs to restart")
|
||||
doRestart = true
|
||||
} else if smtpSSL {
|
||||
log.Debug("Bridge is already using SMTP SSL, no need to restart")
|
||||
} else {
|
||||
log.Debug("OS is pre-catalina (or not darwin at all), no need to change to SMTP SSL")
|
||||
}
|
||||
|
||||
for _, autoConf := range autoconfig.Available() {
|
||||
@ -489,7 +525,7 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
|
||||
|
||||
if doRestart {
|
||||
time.Sleep(2 * time.Second)
|
||||
s.Qml.SetIsRestarting(true)
|
||||
s.restarter.SetToRestart()
|
||||
s.App.Quit()
|
||||
}
|
||||
return
|
||||
@ -498,42 +534,66 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
|
||||
func (s *FrontendQt) toggleAutoStart() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
var err error
|
||||
if s.AutostartEntry.IsEnabled() {
|
||||
err = s.AutostartEntry.Disable()
|
||||
if s.autostart.IsEnabled() {
|
||||
err = s.autostart.Disable()
|
||||
} else {
|
||||
err = s.AutostartEntry.Enable()
|
||||
err = s.autostart.Enable()
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Enable autostart: ", err)
|
||||
s.autostartError(err)
|
||||
}
|
||||
if s.AutostartEntry.IsEnabled() {
|
||||
if s.autostart.IsEnabled() {
|
||||
s.Qml.SetIsAutoStart(true)
|
||||
} else {
|
||||
s.Qml.SetIsAutoStart(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FrontendQt) toggleAutoUpdate() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
|
||||
if s.settings.GetBool(settings.AutoUpdateKey) {
|
||||
s.settings.SetBool(settings.AutoUpdateKey, false)
|
||||
s.Qml.SetIsAutoUpdate(false)
|
||||
} else {
|
||||
s.settings.SetBool(settings.AutoUpdateKey, true)
|
||||
s.Qml.SetIsAutoUpdate(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FrontendQt) toggleEarlyAccess() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
|
||||
if updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel {
|
||||
s.settings.Set(settings.UpdateChannelKey, string(updater.StableChannel))
|
||||
s.Qml.SetIsEarlyAccess(false)
|
||||
} else {
|
||||
s.settings.Set(settings.UpdateChannelKey, string(updater.EarlyChannel))
|
||||
s.Qml.SetIsEarlyAccess(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FrontendQt) toggleAllowProxy() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
|
||||
if s.preferences.GetBool(preferences.AllowProxyKey) {
|
||||
s.preferences.SetBool(preferences.AllowProxyKey, false)
|
||||
if s.settings.GetBool(settings.AllowProxyKey) {
|
||||
s.settings.SetBool(settings.AllowProxyKey, false)
|
||||
s.bridge.DisallowProxy()
|
||||
s.Qml.SetIsProxyAllowed(false)
|
||||
} else {
|
||||
s.preferences.SetBool(preferences.AllowProxyKey, true)
|
||||
s.settings.SetBool(settings.AllowProxyKey, true)
|
||||
s.bridge.AllowProxy()
|
||||
s.Qml.SetIsProxyAllowed(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FrontendQt) getIMAPPort() string {
|
||||
return s.preferences.Get(preferences.IMAPPortKey)
|
||||
return s.settings.Get(settings.IMAPPortKey)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) getSMTPPort() string {
|
||||
return s.preferences.Get(preferences.SMTPPortKey)
|
||||
return s.settings.Get(settings.SMTPPortKey)
|
||||
}
|
||||
|
||||
// Return 0 -- port is free to use for server.
|
||||
@ -550,13 +610,13 @@ func (s *FrontendQt) isPortOpen(portStr string) int {
|
||||
}
|
||||
|
||||
func (s *FrontendQt) setPortsAndSecurity(imapPort, smtpPort string, useSTARTTLSforSMTP bool) {
|
||||
s.preferences.Set(preferences.IMAPPortKey, imapPort)
|
||||
s.preferences.Set(preferences.SMTPPortKey, smtpPort)
|
||||
s.preferences.SetBool(preferences.SMTPSSLKey, !useSTARTTLSforSMTP)
|
||||
s.settings.Set(settings.IMAPPortKey, imapPort)
|
||||
s.settings.Set(settings.SMTPPortKey, smtpPort)
|
||||
s.settings.SetBool(settings.SMTPSSLKey, !useSTARTTLSforSMTP)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) isSMTPSTARTTLS() bool {
|
||||
return !s.preferences.GetBool(preferences.SMTPSSLKey)
|
||||
return !s.settings.GetBool(settings.SMTPSSLKey)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) checkInternet() {
|
||||
@ -594,7 +654,7 @@ func (s *FrontendQt) autostartError(err error) {
|
||||
|
||||
func (s *FrontendQt) toggleIsReportingOutgoingNoEnc() {
|
||||
shouldReport := !s.Qml.IsReportingOutgoingNoEnc()
|
||||
s.preferences.SetBool(preferences.ReportOutgoingNoEncKey, shouldReport)
|
||||
s.settings.SetBool(settings.ReportOutgoingNoEncKey, shouldReport)
|
||||
s.Qml.SetIsReportingOutgoingNoEnc(shouldReport)
|
||||
}
|
||||
|
||||
@ -607,31 +667,15 @@ func (s *FrontendQt) saveOutgoingNoEncPopupCoord(x, y float32) {
|
||||
//prefs.SetFloat(prefs.OutgoingNoEncPopupCoordY, y)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) StartUpdate() {
|
||||
progress := make(chan updates.Progress)
|
||||
go func() { // Update progress in QML.
|
||||
defer s.panicHandler.HandlePanic()
|
||||
for current := range progress {
|
||||
s.Qml.SetProgress(current.Processed)
|
||||
s.Qml.SetProgressDescription(strconv.Itoa(current.Description))
|
||||
// Error happend
|
||||
if current.Err != nil {
|
||||
log.Error("update progress: ", current.Err)
|
||||
s.Qml.UpdateFinished(true)
|
||||
return
|
||||
}
|
||||
// Finished everything OK.
|
||||
if current.Description >= updates.InfoQuitApp {
|
||||
s.Qml.UpdateFinished(false)
|
||||
time.Sleep(3 * time.Second) // Just notify.
|
||||
s.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp)
|
||||
s.App.Quit()
|
||||
return
|
||||
}
|
||||
func (s *FrontendQt) startManualUpdate() {
|
||||
go func() {
|
||||
err := s.updater.InstallUpdate(s.updateInfo)
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("An error occurred while installing updates manually")
|
||||
s.Qml.NotifyManualUpdateError()
|
||||
} else {
|
||||
s.Qml.NotifyManualUpdateRestartNeeded()
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer s.panicHandler.HandlePanic()
|
||||
s.updates.StartUpgrade(progress)
|
||||
}()
|
||||
}
|
||||
|
||||
@ -23,8 +23,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -33,7 +36,7 @@ var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
|
||||
|
||||
type FrontendHeadless struct{}
|
||||
|
||||
func (s *FrontendHeadless) Loop(credentialsError error) error {
|
||||
func (s *FrontendHeadless) Loop() error {
|
||||
log.Info("Check status on localhost:8081")
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Bridge is running")
|
||||
@ -41,20 +44,34 @@ func (s *FrontendHeadless) Loop(credentialsError error) error {
|
||||
return http.ListenAndServe(":8081", nil)
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) InstanceExistAlert() {}
|
||||
func (s *FrontendHeadless) IsAppRestarting() bool { return false }
|
||||
func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
|
||||
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) SetVersion(update updater.VersionInfo) {
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) NotifySilentUpdateInstalled() {
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) NotifySilentUpdateError(err error) {
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) InstanceExistAlert() {}
|
||||
|
||||
func New(
|
||||
version,
|
||||
buildVersion string,
|
||||
buildVersion, appName string,
|
||||
showWindowOnStart bool,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
preferences *config.Preferences,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
restarter types.Restarter,
|
||||
) *FrontendHeadless {
|
||||
return &FrontendHeadless{}
|
||||
}
|
||||
|
||||
@ -653,17 +653,4 @@
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>VersionInfo</name>
|
||||
<message>
|
||||
<location filename="qml/BridgeUI/VersionInfo.qml" line="30"/>
|
||||
<source>Release notes:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="qml/BridgeUI/VersionInfo.qml" line="53"/>
|
||||
<source>Fixed bugs:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
||||
@ -34,6 +34,8 @@ type GoQMLInterface struct {
|
||||
_ func() `constructor:"init"`
|
||||
|
||||
_ bool `property:"isAutoStart"`
|
||||
_ bool `property:"isAutoUpdate"`
|
||||
_ bool `property:"isEarlyAccess"`
|
||||
_ bool `property:"isProxyAllowed"`
|
||||
_ string `property:"currentAddress"`
|
||||
_ string `property:"goos"`
|
||||
@ -41,17 +43,27 @@ type GoQMLInterface struct {
|
||||
_ bool `property:"isShownOnStart"`
|
||||
_ bool `property:"isFirstStart"`
|
||||
_ bool `property:"isFreshVersion"`
|
||||
_ bool `property:"isRestarting"`
|
||||
_ bool `property:"isConnectionOK"`
|
||||
_ bool `property:"isDefaultPort"`
|
||||
|
||||
_ string `property:"programTitle"`
|
||||
_ string `property:"newversion"`
|
||||
_ string `property:"fullversion"`
|
||||
_ string `property:"downloadLink"`
|
||||
_ string `property:"landingPage"`
|
||||
_ string `property:"changelog"`
|
||||
_ string `property:"bugfixes"`
|
||||
|
||||
_ string `property:"updateVersion"`
|
||||
_ bool `property:"updateCanInstall"`
|
||||
_ string `property:"updateLandingPage"`
|
||||
_ string `property:"updateReleaseNotesLink"`
|
||||
_ func() `signal:"notifyManualUpdate"`
|
||||
_ func() `signal:"notifyManualUpdateRestartNeeded"`
|
||||
_ func() `signal:"notifyManualUpdateError"`
|
||||
_ func() `signal:"notifyForceUpdate"`
|
||||
_ func() `signal:"notifySilentUpdateRestartNeeded"`
|
||||
_ func() `signal:"notifySilentUpdateError"`
|
||||
_ func() `slot:"checkForUpdates"`
|
||||
_ func() `slot:"checkAndOpenReleaseNotes"`
|
||||
_ func() `signal:"openReleaseNotesExternally"`
|
||||
_ func() `slot:"startManualUpdate"`
|
||||
|
||||
// Translations.
|
||||
_ string `property:"wrongCredentials"`
|
||||
@ -70,6 +82,8 @@ type GoQMLInterface struct {
|
||||
_ func(updateState string) `signal:"setUpdateState"`
|
||||
_ func() `slot:"checkInternet"`
|
||||
|
||||
_ func() `slot:"setToRestart"`
|
||||
|
||||
_ func(systX, systY, systW, systH int) `signal:"toggleMainWin"`
|
||||
|
||||
_ func() `signal:"processFinished"`
|
||||
@ -82,6 +96,8 @@ type GoQMLInterface struct {
|
||||
_ func() `signal:"showQuit"`
|
||||
|
||||
_ func() `slot:"toggleAutoStart"`
|
||||
_ func() `slot:"toggleAutoUpdate"`
|
||||
_ func() `slot:"toggleEarlyAccess"`
|
||||
_ func() `slot:"toggleAllowProxy"`
|
||||
_ func() `slot:"loadAccounts"`
|
||||
_ func() `slot:"openLogs"`
|
||||
@ -121,7 +137,6 @@ type GoQMLInterface struct {
|
||||
_ func() `signal:"notifyVersionIsTheLatest"`
|
||||
_ func() `signal:"notifyKeychainRebuild"`
|
||||
_ func() `signal:"notifyHasNoKeychain"`
|
||||
_ func() `signal:"notifyUpdate"`
|
||||
_ func(accname string) `signal:"notifyLogout"`
|
||||
_ func(accname string) `signal:"notifyAddressChanged"`
|
||||
_ func(accname string) `signal:"notifyAddressChangedLogout"`
|
||||
@ -137,7 +152,6 @@ type GoQMLInterface struct {
|
||||
_ func(recipient string) `signal:"showNoActiveKeyForRecipient"`
|
||||
_ func() `signal:"showCertIssue"`
|
||||
|
||||
_ func() `slot:"startUpdate"`
|
||||
_ func(hasError bool) `signal:"updateFinished"`
|
||||
}
|
||||
|
||||
@ -147,15 +161,17 @@ func (s *GoQMLInterface) init() {}
|
||||
// SetFrontend connects all slots and signals from Go to QML.
|
||||
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectToggleAutoStart(f.toggleAutoStart)
|
||||
s.ConnectToggleEarlyAccess(f.toggleEarlyAccess)
|
||||
s.ConnectToggleAutoUpdate(f.toggleAutoUpdate)
|
||||
s.ConnectToggleAllowProxy(f.toggleAllowProxy)
|
||||
s.ConnectLoadAccounts(f.loadAccounts)
|
||||
s.ConnectOpenLogs(f.openLogs)
|
||||
s.ConnectClearCache(f.clearCache)
|
||||
s.ConnectClearKeychain(f.clearKeychain)
|
||||
|
||||
s.ConnectOpenLicenseFile(f.openLicenseFile)
|
||||
s.ConnectStartManualUpdate(f.startManualUpdate)
|
||||
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
|
||||
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
|
||||
s.ConnectCheckForUpdates(f.checkForUpdates)
|
||||
s.ConnectGetIMAPPort(f.getIMAPPort)
|
||||
s.ConnectGetSMTPPort(f.getSMTPPort)
|
||||
s.ConnectGetLastMailClient(f.getLastMailClient)
|
||||
@ -178,7 +194,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectSwitchAddressMode(f.switchAddressModeUser)
|
||||
|
||||
s.SetGoos(runtime.GOOS)
|
||||
s.SetIsRestarting(false)
|
||||
s.SetProgramTitle(f.programName)
|
||||
|
||||
s.ConnectGetBackendVersion(func() string {
|
||||
@ -187,8 +202,9 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
|
||||
s.ConnectCheckInternet(f.checkInternet)
|
||||
|
||||
s.ConnectSetToRestart(f.restarter.SetToRestart)
|
||||
|
||||
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)
|
||||
s.ConnectShouldSendAnswer(f.shouldSendAnswer)
|
||||
s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord)
|
||||
s.ConnectStartUpdate(f.StartUpdate)
|
||||
}
|
||||
|
||||
@ -71,7 +71,6 @@
|
||||
<file alias="OutgoingNoEncPopup.qml" >./qml/BridgeUI/OutgoingNoEncPopup.qml</file>
|
||||
<file alias="SettingsView.qml" >./qml/BridgeUI/SettingsView.qml</file>
|
||||
<file alias="StatusFooter.qml" >./qml/BridgeUI/StatusFooter.qml</file>
|
||||
<file alias="VersionInfo.qml" >./qml/BridgeUI/VersionInfo.qml</file>
|
||||
</qresource>
|
||||
<qresource prefix="ImportExportUI">
|
||||
<file alias="qmldir" >./qml/ImportExportUI/qmldir</file>
|
||||
@ -104,7 +103,6 @@
|
||||
<file alias="SelectFolderMenu.qml" >./qml/ImportExportUI/SelectFolderMenu.qml</file>
|
||||
<file alias="SelectLabelsMenu.qml" >./qml/ImportExportUI/SelectLabelsMenu.qml</file>
|
||||
<file alias="SettingsView.qml" >./qml/ImportExportUI/SettingsView.qml</file>
|
||||
<file alias="VersionInfo.qml" >./qml/ImportExportUI/VersionInfo.qml</file>
|
||||
<file alias="images/folder_open.png" >./share/icons/folder_open.png</file>
|
||||
<file alias="images/envelope_open.png" >./share/icons/envelope_open.png</file>
|
||||
</qresource>
|
||||
|
||||
@ -22,7 +22,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
@ -31,18 +31,22 @@ type PanicHandler interface {
|
||||
HandlePanic()
|
||||
}
|
||||
|
||||
// Updater is an interface for handling Bridge upgrades.
|
||||
type Updater interface {
|
||||
CheckIsUpToDate() (isUpToDate bool, latestVersion updates.VersionInfo, err error)
|
||||
GetDownloadLink() string
|
||||
GetLocalVersion() updates.VersionInfo
|
||||
StartUpgrade(currentStatus chan<- updates.Progress)
|
||||
// Restarter allows the app to set itself to restart next time it is closed.
|
||||
type Restarter interface {
|
||||
SetToRestart()
|
||||
}
|
||||
|
||||
type NoEncConfirmator interface {
|
||||
ConfirmNoEncryption(string, bool)
|
||||
}
|
||||
|
||||
type Updater interface {
|
||||
Check() (updater.VersionInfo, error)
|
||||
InstallUpdate(updater.VersionInfo) error
|
||||
IsUpdateApplicable(updater.VersionInfo) bool
|
||||
CanInstall(updater.VersionInfo) bool
|
||||
}
|
||||
|
||||
// UserManager is an interface of users needed by frontend.
|
||||
type UserManager interface {
|
||||
Login(username, password string) (pmapi.Client, *pmapi.Auth, error)
|
||||
|
||||
@ -37,7 +37,7 @@ type panicHandler interface {
|
||||
type imapBackend struct {
|
||||
panicHandler panicHandler
|
||||
bridge bridger
|
||||
updates chan goIMAPBackend.Update
|
||||
updates *imapUpdates
|
||||
eventListener listener.Listener
|
||||
|
||||
users map[string]*imapUser
|
||||
@ -46,20 +46,17 @@ type imapBackend struct {
|
||||
imapCache map[string]map[string]string
|
||||
imapCachePath string
|
||||
imapCacheLock *sync.RWMutex
|
||||
|
||||
updatesBlocking map[string]bool
|
||||
updatesBlockingLocker sync.Locker
|
||||
}
|
||||
|
||||
// NewIMAPBackend returns struct implementing go-imap/backend interface.
|
||||
func NewIMAPBackend(
|
||||
panicHandler panicHandler,
|
||||
eventListener listener.Listener,
|
||||
cfg configProvider,
|
||||
cache cacheProvider,
|
||||
bridge *bridge.Bridge,
|
||||
) *imapBackend { //nolint[golint]
|
||||
bridgeWrap := newBridgeWrap(bridge)
|
||||
backend := newIMAPBackend(panicHandler, cfg, bridgeWrap, eventListener)
|
||||
backend := newIMAPBackend(panicHandler, cache, bridgeWrap, eventListener)
|
||||
|
||||
go backend.monitorDisconnectedUsers()
|
||||
|
||||
@ -68,24 +65,21 @@ func NewIMAPBackend(
|
||||
|
||||
func newIMAPBackend(
|
||||
panicHandler panicHandler,
|
||||
cfg configProvider,
|
||||
cache cacheProvider,
|
||||
bridge bridger,
|
||||
eventListener listener.Listener,
|
||||
) *imapBackend {
|
||||
return &imapBackend{
|
||||
panicHandler: panicHandler,
|
||||
bridge: bridge,
|
||||
updates: make(chan goIMAPBackend.Update),
|
||||
updates: newIMAPUpdates(),
|
||||
eventListener: eventListener,
|
||||
|
||||
users: map[string]*imapUser{},
|
||||
usersLocker: &sync.Mutex{},
|
||||
|
||||
imapCachePath: cfg.GetIMAPCachePath(),
|
||||
imapCachePath: cache.GetIMAPCachePath(),
|
||||
imapCacheLock: &sync.RWMutex{},
|
||||
|
||||
updatesBlocking: map[string]bool{},
|
||||
updatesBlockingLocker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,7 +166,7 @@ func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMA
|
||||
// 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)
|
||||
store.SetChangeNotifier(ib.updates)
|
||||
}
|
||||
|
||||
return imapUser, nil
|
||||
@ -183,7 +177,7 @@ 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
|
||||
return ib.updates.ch
|
||||
}
|
||||
|
||||
func (ib *imapBackend) CreateMessageLimit() *uint32 {
|
||||
|
||||
@ -23,8 +23,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type configProvider interface {
|
||||
GetEventsPath() string
|
||||
type cacheProvider interface {
|
||||
GetDBDir() string
|
||||
GetIMAPCachePath() string
|
||||
}
|
||||
|
||||
8
internal/imap/cache/cache.go
vendored
8
internal/imap/cache/cache.go
vendored
@ -23,7 +23,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
backendMessage "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
)
|
||||
|
||||
type key struct {
|
||||
@ -41,7 +41,7 @@ func (s oldestFirst) Less(i, j int) bool { return s[i].Timestamp < s[j].Timestam
|
||||
type cachedMessage struct {
|
||||
key
|
||||
data []byte
|
||||
structure backendMessage.BodyStructure
|
||||
structure pkgMsg.BodyStructure
|
||||
}
|
||||
|
||||
//nolint[gochecknoglobals]
|
||||
@ -101,7 +101,7 @@ func BuildUnlock(messageID string) {
|
||||
delete(buildLocks, messageID)
|
||||
}
|
||||
|
||||
func LoadMail(mID string) (reader *bytes.Reader, structure *backendMessage.BodyStructure) {
|
||||
func LoadMail(mID string) (reader *bytes.Reader, structure *pkgMsg.BodyStructure) {
|
||||
reader = &bytes.Reader{}
|
||||
cacheMutex.Lock()
|
||||
defer cacheMutex.Unlock()
|
||||
@ -115,7 +115,7 @@ func LoadMail(mID string) (reader *bytes.Reader, structure *backendMessage.BodyS
|
||||
return
|
||||
}
|
||||
|
||||
func SaveMail(mID string, msg []byte, structure *backendMessage.BodyStructure) {
|
||||
func SaveMail(mID string, msg []byte, structure *pkgMsg.BodyStructure) {
|
||||
cacheMutex.Lock()
|
||||
defer cacheMutex.Unlock()
|
||||
|
||||
|
||||
4
internal/imap/cache/cache_test.go
vendored
4
internal/imap/cache/cache_test.go
vendored
@ -22,11 +22,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
bckMsg "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var bs = &bckMsg.BodyStructure{} //nolint[gochecknoglobals]
|
||||
var bs = &pkgMsg.BodyStructure{} //nolint[gochecknoglobals]
|
||||
const testUID = "testmsg"
|
||||
|
||||
func TestSaveAndLoad(t *testing.T) {
|
||||
|
||||
@ -188,10 +188,23 @@ func (im *imapMailbox) Expunge() error {
|
||||
// the desired mailbox.
|
||||
im.user.waitForAppend()
|
||||
|
||||
im.user.backend.setUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
|
||||
defer im.user.backend.unsetUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
|
||||
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()
|
||||
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 {
|
||||
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) {
|
||||
|
||||
@ -166,6 +166,13 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
|
||||
if err == nil && (im.user.user.IsCombinedAddressMode() || (im.storeAddress.AddressID() == msg.Message().AddressID)) {
|
||||
IDs := []string{internalID}
|
||||
|
||||
// See the comment bellow.
|
||||
if msg.IsMarkedDeleted() {
|
||||
if err := im.storeMailbox.MarkMessagesUndeleted(IDs); err != nil {
|
||||
log.WithError(err).Error("Failed to undelete re-imported internal message")
|
||||
}
|
||||
}
|
||||
|
||||
err = im.storeMailbox.LabelMessages(IDs)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -182,6 +189,20 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
msg, err := im.storeMailbox.GetMessage(m.ID)
|
||||
if err == nil && msg.IsMarkedDeleted() {
|
||||
if err := im.storeMailbox.MarkMessagesUndeleted([]string{m.ID}); err != nil {
|
||||
log.WithError(err).Error("Failed to undelete re-imported message")
|
||||
}
|
||||
}
|
||||
|
||||
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
|
||||
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
|
||||
}
|
||||
@ -219,7 +240,8 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
|
||||
msg.Envelope = message.GetEnvelope(m)
|
||||
case imap.FetchBody, imap.FetchBodyStructure:
|
||||
var structure *message.BodyStructure
|
||||
if structure, _, err = im.getBodyStructure(storeMessage); err != nil {
|
||||
structure, err = im.getBodyStructure(storeMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
|
||||
@ -242,7 +264,8 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
|
||||
// Size attribute on the server counts encrypted data. The value is cleared
|
||||
// on our part and we need to compute "real" size of decrypted data.
|
||||
if m.Size <= 0 {
|
||||
if _, _, err = im.getBodyStructure(storeMessage); err != nil {
|
||||
im.log.WithField("msgID", storeMessage.ID()).Trace("Size unknown - downloading body")
|
||||
if _, _, err = im.getBodyAndStructure(storeMessage); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -277,7 +300,24 @@ func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *ima
|
||||
return nil
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (
|
||||
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *message.BodyStructure, err error) {
|
||||
// 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.
|
||||
bs, err = storeMessage.GetBodyStructure()
|
||||
if err != nil {
|
||||
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
|
||||
}
|
||||
if bs == nil {
|
||||
if bs, _, err = im.getBodyAndStructure(storeMessage); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) (
|
||||
structure *message.BodyStructure,
|
||||
bodyReader *bytes.Reader, err error,
|
||||
) {
|
||||
@ -287,14 +327,23 @@ func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (
|
||||
if bodyReader, structure = cache.LoadMail(id); bodyReader.Len() == 0 || structure == nil {
|
||||
var body []byte
|
||||
structure, body, err = im.buildMessage(m)
|
||||
if err == nil && structure != nil && len(body) > 0 {
|
||||
m.Size = int64(len(body))
|
||||
if err := storeMessage.SetSize(m.Size); err != nil {
|
||||
m.Size = int64(len(body))
|
||||
// Save size and body structure even for messages unable to decrypt
|
||||
// so the size or body structure doesn't have to be computed every time.
|
||||
if err := storeMessage.SetSize(m.Size); err != nil {
|
||||
im.log.WithError(err).
|
||||
WithField("newSize", m.Size).
|
||||
WithField("msgID", m.ID).
|
||||
Warn("Cannot update size while building")
|
||||
}
|
||||
if structure != nil && !isMessageInDraftFolder(m) {
|
||||
if err := storeMessage.SetBodyStructure(structure); err != nil {
|
||||
im.log.WithError(err).
|
||||
WithField("newSize", m.Size).
|
||||
WithField("msgID", m.ID).
|
||||
Warn("Cannot update size while building")
|
||||
Warn("Cannot update bodystructure while building")
|
||||
}
|
||||
}
|
||||
if err == nil && structure != nil && len(body) > 0 {
|
||||
if err := storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil {
|
||||
im.log.WithError(err).
|
||||
WithField("msgID", m.ID).
|
||||
@ -358,7 +407,7 @@ func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider,
|
||||
}
|
||||
} else {
|
||||
// The rest of cases need download and decrypt.
|
||||
structure, bodyReader, err = im.getBodyStructure(storeMessage)
|
||||
structure, bodyReader, err = im.getBodyAndStructure(storeMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -46,8 +46,8 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
im.user.backend.setUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationUpdateMessage)
|
||||
defer im.user.backend.unsetUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationUpdateMessage)
|
||||
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 {
|
||||
@ -277,7 +277,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
}
|
||||
|
||||
if criteria.Body != nil || criteria.Text != nil {
|
||||
log.Warn("Body and Text criteria not applied.")
|
||||
log.Warn("Body and Text criteria not applied")
|
||||
}
|
||||
|
||||
var apiIDs []string
|
||||
@ -451,11 +451,13 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
|
||||
im.panicHandler.HandlePanic()
|
||||
}()
|
||||
|
||||
// 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.storeUser.PauseEventLoop(true)
|
||||
defer im.storeUser.PauseEventLoop(false)
|
||||
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{}
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
imapid "github.com/ProtonMail/go-imap-id"
|
||||
@ -31,6 +32,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/id"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/emersion/go-imap"
|
||||
imapappendlimit "github.com/emersion/go-imap-appendlimit"
|
||||
imapidle "github.com/emersion/go-imap-idle"
|
||||
@ -43,14 +45,17 @@ import (
|
||||
)
|
||||
|
||||
type imapServer struct {
|
||||
panicHandler panicHandler
|
||||
server *imapserver.Server
|
||||
eventListener listener.Listener
|
||||
debugClient bool
|
||||
debugServer bool
|
||||
port int
|
||||
isRunning atomic.Value
|
||||
}
|
||||
|
||||
// NewIMAPServer constructs a new IMAP server configured with the given options.
|
||||
func NewIMAPServer(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 *imapBackend, eventListener listener.Listener) *imapServer { //nolint[golint]
|
||||
s := imapserver.New(imapBackend)
|
||||
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
|
||||
s.TLSConfig = tls
|
||||
@ -58,6 +63,13 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
s.ErrorLog = newServerErrorLogger("server-imap")
|
||||
s.AutoLogout = 30 * time.Minute
|
||||
|
||||
if debugServer {
|
||||
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
log.Warning("================================================")
|
||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
log.Warning("================================================")
|
||||
}
|
||||
|
||||
serverID := imapid.ID{
|
||||
imapid.FieldName: "ProtonMail Bridge",
|
||||
imapid.FieldVendor: "Proton Technologies AG",
|
||||
@ -88,17 +100,31 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
uidplus.NewExtension(),
|
||||
)
|
||||
|
||||
return &imapServer{
|
||||
server := &imapServer{
|
||||
panicHandler: panicHandler,
|
||||
server: s,
|
||||
eventListener: eventListener,
|
||||
debugClient: debugClient,
|
||||
debugServer: debugServer,
|
||||
port: port,
|
||||
}
|
||||
server.isRunning.Store(false)
|
||||
return server
|
||||
}
|
||||
|
||||
// Starts the server.
|
||||
func (s *imapServer) ListenAndServe() {
|
||||
go s.monitorDisconnectedUsers()
|
||||
go s.monitorInternetConnection()
|
||||
|
||||
s.listenAndServe()
|
||||
}
|
||||
|
||||
func (s *imapServer) listenAndServe() {
|
||||
if s.isRunning.Load().(bool) {
|
||||
return
|
||||
}
|
||||
s.isRunning.Store(true)
|
||||
|
||||
log.Info("IMAP server listening at ", s.server.Addr)
|
||||
l, err := net.Listen("tcp", s.server.Addr)
|
||||
@ -112,7 +138,10 @@ func (s *imapServer) ListenAndServe() {
|
||||
Listener: l,
|
||||
server: s,
|
||||
})
|
||||
if err != nil {
|
||||
// Serve returns error every time, even after closing the server.
|
||||
// User shouldn't be notified about error if server shouldn't be running,
|
||||
// but it should in case it was not closed by `s.Close()`.
|
||||
if err != nil && s.isRunning.Load().(bool) {
|
||||
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
|
||||
log.Error("IMAP failed: ", err)
|
||||
return
|
||||
@ -124,11 +153,52 @@ func (s *imapServer) ListenAndServe() {
|
||||
|
||||
// Stops the server.
|
||||
func (s *imapServer) Close() {
|
||||
if !s.isRunning.Load().(bool) {
|
||||
return
|
||||
}
|
||||
s.isRunning.Store(false)
|
||||
|
||||
log.Info("Closing IMAP server")
|
||||
if err := s.server.Close(); err != nil {
|
||||
log.WithError(err).Error("Failed to close the connection")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *imapServer) monitorInternetConnection() {
|
||||
on := make(chan string)
|
||||
s.eventListener.Add(events.InternetOnEvent, on)
|
||||
off := make(chan string)
|
||||
s.eventListener.Add(events.InternetOffEvent, off)
|
||||
|
||||
for {
|
||||
var expectedIsPortFree bool
|
||||
select {
|
||||
case <-on:
|
||||
go func() {
|
||||
defer s.panicHandler.HandlePanic()
|
||||
s.listenAndServe()
|
||||
}()
|
||||
expectedIsPortFree = false
|
||||
case <-off:
|
||||
s.Close()
|
||||
expectedIsPortFree = true
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
for {
|
||||
if ports.IsPortFree(s.port) == expectedIsPortFree {
|
||||
break
|
||||
}
|
||||
// Safety stop if something went wrong.
|
||||
if time.Since(start) > 15*time.Second {
|
||||
log.WithField("expectedIsPortFree", expectedIsPortFree).Warn("Server start/stop check timeouted")
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *imapServer) monitorDisconnectedUsers() {
|
||||
ch := make(chan string)
|
||||
s.eventListener.Add(events.CloseConnectionEvent, ch)
|
||||
|
||||
65
internal/imap/server_test.go
Normal file
65
internal/imap/server_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
// 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 imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
imapserver "github.com/emersion/go-imap/server"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testPanicHandler struct{}
|
||||
|
||||
func (ph *testPanicHandler) HandlePanic() {}
|
||||
|
||||
func TestIMAPServerTurnOffAndOnAgain(t *testing.T) {
|
||||
panicHandler := &testPanicHandler{}
|
||||
|
||||
eventListener := listener.New()
|
||||
|
||||
port := ports.FindFreePortFrom(12345)
|
||||
server := imapserver.New(nil)
|
||||
server.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
|
||||
|
||||
s := &imapServer{
|
||||
panicHandler: panicHandler,
|
||||
server: server,
|
||||
eventListener: eventListener,
|
||||
}
|
||||
s.isRunning.Store(false)
|
||||
|
||||
go s.ListenAndServe()
|
||||
time.Sleep(5 * time.Second)
|
||||
require.False(t, ports.IsPortFree(port))
|
||||
|
||||
eventListener.Emit(events.InternetOffEvent, "")
|
||||
time.Sleep(10 * time.Second)
|
||||
require.True(t, ports.IsPortFree(port))
|
||||
|
||||
eventListener.Emit(events.InternetOnEvent, "")
|
||||
time.Sleep(10 * time.Second)
|
||||
require.False(t, ports.IsPortFree(port))
|
||||
}
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
@ -42,8 +43,6 @@ type storeUserProvider interface {
|
||||
attachedPublicKeyName string,
|
||||
parentID string) (*pmapi.Message, []*pmapi.Attachment, error)
|
||||
|
||||
PauseEventLoop(bool)
|
||||
|
||||
SetChangeNotifier(store.ChangeNotifier)
|
||||
}
|
||||
|
||||
@ -89,7 +88,7 @@ type storeMailboxProvider interface {
|
||||
MarkMessagesDeleted(apiID []string) error
|
||||
MarkMessagesUndeleted(apiID []string) error
|
||||
ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error
|
||||
RemoveDeleted() error
|
||||
RemoveDeleted(apiIDs []string) error
|
||||
}
|
||||
|
||||
type storeMessageProvider interface {
|
||||
@ -101,6 +100,8 @@ type storeMessageProvider interface {
|
||||
|
||||
SetSize(int64) error
|
||||
SetContentTypeAndHeader(string, mail.Header) error
|
||||
SetBodyStructure(*pkgMsg.BodyStructure) error
|
||||
GetBodyStructure() (*pkgMsg.BodyStructure, error)
|
||||
}
|
||||
|
||||
type storeUserWrap struct {
|
||||
|
||||
@ -116,40 +116,70 @@ func (os *OrderedSeq) String() string {
|
||||
|
||||
// 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 {
|
||||
expunge *server.Expunge
|
||||
SeqSet *imap.SeqSet
|
||||
}
|
||||
|
||||
func newUIDExpunge() *UIDExpunge {
|
||||
return &UIDExpunge{expunge: &server.Expunge{}}
|
||||
return &UIDExpunge{}
|
||||
}
|
||||
|
||||
func (e *UIDExpunge) Parse(fields []interface{}) error {
|
||||
if len(fields) < 1 {
|
||||
return e.expunge.Parse(fields)
|
||||
if len(fields) == 0 {
|
||||
return nil // It could be regular EXPUNGE without arguments.
|
||||
}
|
||||
if len(fields) > 1 {
|
||||
return errors.New("too many arguments")
|
||||
}
|
||||
|
||||
// RFC4315#section-2.1
|
||||
// The UID EXPUNGE command permanently removes all messages that both
|
||||
// have the \Deleted flag set and have a UID that is included in the
|
||||
// specified sequence set from the currently selected mailbox. If a
|
||||
// message either does not have the \Deleted flag set or has a UID
|
||||
// that is not included in the specified sequence set, it is not
|
||||
// affected.
|
||||
//
|
||||
// Current implementation supports only deletion of all messages
|
||||
// marked as deleted. It will probably need mailbox interface change:
|
||||
// ExpungeUIDs(seqSet). Not sure how to combine with original
|
||||
// e.expunge.Handle().
|
||||
return errors.New("UID EXPUNGE with UIDs is not supported")
|
||||
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 {
|
||||
return e.expunge.Handle(conn)
|
||||
mailbox, err := e.getMailbox(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mailbox.Expunge()
|
||||
}
|
||||
|
||||
func (e *UIDExpunge) UidHandle(conn server.Conn) error { //nolint[golint]
|
||||
return e.expunge.Handle(conn)
|
||||
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{}
|
||||
|
||||
@ -19,6 +19,7 @@ package imap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
@ -36,32 +37,82 @@ const (
|
||||
operationDeleteMessage operation = "expunge"
|
||||
)
|
||||
|
||||
func (ib *imapBackend) setUpdatesBeBlocking(address, mailboxName string, op operation) {
|
||||
ib.changeUpdatesBlocking(address, mailboxName, op, true)
|
||||
type imapUpdates struct {
|
||||
lock sync.Locker
|
||||
blocking map[string]bool
|
||||
delayedExpunges map[string][]chan struct{}
|
||||
ch chan goIMAPBackend.Update
|
||||
}
|
||||
|
||||
func (ib *imapBackend) unsetUpdatesBeBlocking(address, mailboxName string, op operation) {
|
||||
ib.changeUpdatesBlocking(address, mailboxName, op, false)
|
||||
}
|
||||
|
||||
func (ib *imapBackend) changeUpdatesBlocking(address, mailboxName string, op operation, block bool) {
|
||||
ib.updatesBlockingLocker.Lock()
|
||||
defer ib.updatesBlockingLocker.Unlock()
|
||||
|
||||
key := strings.ToLower(address + "_" + mailboxName + "_" + string(op))
|
||||
if block {
|
||||
ib.updatesBlocking[key] = true
|
||||
} else {
|
||||
delete(ib.updatesBlocking, key)
|
||||
func newIMAPUpdates() *imapUpdates {
|
||||
return &imapUpdates{
|
||||
lock: &sync.Mutex{},
|
||||
blocking: map[string]bool{},
|
||||
delayedExpunges: map[string][]chan struct{}{},
|
||||
ch: make(chan goIMAPBackend.Update),
|
||||
}
|
||||
}
|
||||
|
||||
func (ib *imapBackend) isBlocking(address, mailboxName string, op operation) bool {
|
||||
key := strings.ToLower(address + "_" + mailboxName + "_" + string(op))
|
||||
return ib.updatesBlocking[key]
|
||||
func (iu *imapUpdates) block(address, mailboxName string, op operation) {
|
||||
iu.lock.Lock()
|
||||
defer iu.lock.Unlock()
|
||||
|
||||
iu.blocking[getBlockingKey(address, mailboxName, op)] = true
|
||||
}
|
||||
|
||||
func (ib *imapBackend) Notice(address, notice string) {
|
||||
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) {
|
||||
update := new(goIMAPBackend.StatusUpdate)
|
||||
update.Update = goIMAPBackend.NewUpdate(address, "")
|
||||
update.StatusResp = &imap.StatusResp{
|
||||
@ -69,10 +120,10 @@ func (ib *imapBackend) Notice(address, notice string) {
|
||||
Code: imap.CodeAlert,
|
||||
Info: notice,
|
||||
}
|
||||
ib.sendIMAPUpdate(update, false)
|
||||
iu.sendIMAPUpdate(update, false)
|
||||
}
|
||||
|
||||
func (ib *imapBackend) UpdateMessage(
|
||||
func (iu *imapUpdates) UpdateMessage(
|
||||
address, mailboxName string,
|
||||
uid, sequenceNumber uint32,
|
||||
msg *pmapi.Message, hasDeletedFlag bool,
|
||||
@ -93,10 +144,10 @@ func (ib *imapBackend) UpdateMessage(
|
||||
update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag)
|
||||
}
|
||||
update.Message.Uid = uid
|
||||
ib.sendIMAPUpdate(update, ib.isBlocking(address, mailboxName, operationUpdateMessage))
|
||||
iu.sendIMAPUpdate(update, iu.isBlocking(address, mailboxName, operationUpdateMessage))
|
||||
}
|
||||
|
||||
func (ib *imapBackend) DeleteMessage(address, mailboxName string, sequenceNumber uint32) {
|
||||
func (iu *imapUpdates) DeleteMessage(address, mailboxName string, sequenceNumber uint32) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
@ -105,10 +156,10 @@ func (ib *imapBackend) DeleteMessage(address, mailboxName string, sequenceNumber
|
||||
update := new(goIMAPBackend.ExpungeUpdate)
|
||||
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
|
||||
update.SeqNum = sequenceNumber
|
||||
ib.sendIMAPUpdate(update, ib.isBlocking(address, mailboxName, operationDeleteMessage))
|
||||
iu.sendIMAPUpdate(update, iu.isBlocking(address, mailboxName, operationDeleteMessage))
|
||||
}
|
||||
|
||||
func (ib *imapBackend) MailboxCreated(address, mailboxName string) {
|
||||
func (iu *imapUpdates) MailboxCreated(address, mailboxName string) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
@ -120,10 +171,10 @@ func (ib *imapBackend) MailboxCreated(address, mailboxName string) {
|
||||
Delimiter: store.PathDelimiter,
|
||||
Name: mailboxName,
|
||||
}
|
||||
ib.sendIMAPUpdate(update, false)
|
||||
iu.sendIMAPUpdate(update, false)
|
||||
}
|
||||
|
||||
func (ib *imapBackend) MailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint32) {
|
||||
func (iu *imapUpdates) MailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint32) {
|
||||
log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
@ -137,11 +188,11 @@ func (ib *imapBackend) MailboxStatus(address, mailboxName string, total, unread,
|
||||
update.MailboxStatus.Messages = total
|
||||
update.MailboxStatus.Unseen = unread
|
||||
update.MailboxStatus.UnseenSeqNum = unreadSeqNum
|
||||
ib.sendIMAPUpdate(update, false)
|
||||
iu.sendIMAPUpdate(update, false)
|
||||
}
|
||||
|
||||
func (ib *imapBackend) sendIMAPUpdate(update goIMAPBackend.Update, block bool) {
|
||||
if ib.updates == nil {
|
||||
func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, block bool) {
|
||||
if iu.ch == nil {
|
||||
log.Trace("IMAP IDLE unavailable")
|
||||
return
|
||||
}
|
||||
@ -152,7 +203,7 @@ func (ib *imapBackend) sendIMAPUpdate(update goIMAPBackend.Update, block bool) {
|
||||
case <-time.After(1 * time.Second):
|
||||
log.Warn("IMAP update could not be sent (timeout)")
|
||||
return
|
||||
case ib.updates <- update:
|
||||
case iu.ch <- update:
|
||||
}
|
||||
}()
|
||||
|
||||
@ -163,7 +214,7 @@ func (ib *imapBackend) sendIMAPUpdate(update goIMAPBackend.Update, block bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(1 * time.Second):
|
||||
log.Warn("IMAP update could not be delivered (timeout).")
|
||||
log.Warn("IMAP update could not be delivered (timeout)")
|
||||
return
|
||||
}
|
||||
}
|
||||
60
internal/imap/updates_test.go
Normal file
60
internal/imap/updates_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 imap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdatesCanDelete(t *testing.T) {
|
||||
u := newIMAPUpdates()
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
}
|
||||
@ -44,11 +44,6 @@ type imapUser struct {
|
||||
appendInProcess sync.WaitGroup
|
||||
}
|
||||
|
||||
// This method should eventually no longer be necessary. Everything should go via store.
|
||||
func (iu *imapUser) client() pmapi.Client {
|
||||
return iu.user.GetTemporaryPMAPIClient()
|
||||
}
|
||||
|
||||
// newIMAPUser returns struct implementing go-imap/user interface.
|
||||
func newIMAPUser(
|
||||
panicHandler panicHandler,
|
||||
@ -81,6 +76,11 @@ func newIMAPUser(
|
||||
}, err
|
||||
}
|
||||
|
||||
// This method should eventually no longer be necessary. Everything should go via store.
|
||||
func (iu *imapUser) client() pmapi.Client {
|
||||
return iu.user.GetTemporaryPMAPIClient()
|
||||
}
|
||||
|
||||
func (iu *imapUser) isSubscribed(labelID string) bool {
|
||||
subscriptionExceptions := iu.backend.getCacheList(iu.storeUser.UserID(), SubscriptionException)
|
||||
exceptions := strings.Split(subscriptionExceptions, ";")
|
||||
@ -139,7 +139,16 @@ func (iu *imapUser) GetMailbox(name string) (mb goIMAPBackend.Mailbox, err error
|
||||
|
||||
storeMailbox, err := iu.storeAddress.GetMailbox(name)
|
||||
if err != nil {
|
||||
log.WithField("name", name).WithError(err).Error("Could not get mailbox")
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at Mon Dec 28 02:39:43 PM CET 2020. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Fri Jan 22 11:28:55 CET 2021. DO NOT EDIT.
|
||||
|
||||
package importexport
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;github.com/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
|
||||
@ -36,23 +36,27 @@ var (
|
||||
type ImportExport struct {
|
||||
*users.Users
|
||||
|
||||
config Configer
|
||||
locations Locator
|
||||
cache Cacher
|
||||
panicHandler users.PanicHandler
|
||||
clientManager users.ClientManager
|
||||
}
|
||||
|
||||
func New(
|
||||
config Configer,
|
||||
locations Locator,
|
||||
cache Cacher,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
clientManager users.ClientManager,
|
||||
credStorer users.CredentialsStorer,
|
||||
) *ImportExport {
|
||||
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, &storeFactory{}, false)
|
||||
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, &storeFactory{}, false)
|
||||
|
||||
return &ImportExport{
|
||||
Users: u,
|
||||
|
||||
config: config,
|
||||
locations: locations,
|
||||
cache: cache,
|
||||
panicHandler: panicHandler,
|
||||
clientManager: clientManager,
|
||||
}
|
||||
@ -120,7 +124,11 @@ func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transf
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
|
||||
logsPath, err := ie.locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
// GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account.
|
||||
@ -133,7 +141,11 @@ func (ie *ImportExport) GetRemoteImporter(address, username, password, host, por
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
|
||||
logsPath, err := ie.locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
// GetEMLExporter returns transferrer from ProtonMail account to local EML structure.
|
||||
@ -143,7 +155,11 @@ func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer
|
||||
return nil, err
|
||||
}
|
||||
target := transfer.NewEMLProvider(path)
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
|
||||
logsPath, err := ie.locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
// GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure.
|
||||
@ -153,7 +169,11 @@ func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfe
|
||||
return nil, err
|
||||
}
|
||||
target := transfer.NewMBOXProvider(path)
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
|
||||
logsPath, err := ie.locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvider, error) {
|
||||
@ -167,5 +187,5 @@ func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvide
|
||||
log.WithError(err).Info("Address does not exist, using all addresses")
|
||||
}
|
||||
|
||||
return transfer.NewPMAPIProvider(ie.config.GetAPIConfig(), ie.clientManager, user.ID(), addressID)
|
||||
return transfer.NewPMAPIProvider(ie.clientManager, user.ID(), addressID)
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type metricsManager struct {
|
||||
@ -44,21 +45,31 @@ func newExportMetricsManager(ie *ImportExport) *metricsManager {
|
||||
|
||||
func (m *metricsManager) Load(numberOfMailboxes int) {
|
||||
label := strconv.Itoa(numberOfMailboxes)
|
||||
m.ie.SendMetric(metrics.New(m.category, metrics.TransferLoad, metrics.Label(label)))
|
||||
if err := m.ie.SendMetric(metrics.New(m.category, metrics.TransferLoad, metrics.Label(label))); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *metricsManager) Start() {
|
||||
m.ie.SendMetric(metrics.New(m.category, metrics.TransferStart, metrics.NoLabel))
|
||||
if err := m.ie.SendMetric(metrics.New(m.category, metrics.TransferStart, metrics.NoLabel)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *metricsManager) Complete() {
|
||||
m.ie.SendMetric(metrics.New(m.category, metrics.TransferComplete, metrics.NoLabel))
|
||||
if err := m.ie.SendMetric(metrics.New(m.category, metrics.TransferComplete, metrics.NoLabel)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *metricsManager) Cancel() {
|
||||
m.ie.SendMetric(metrics.New(m.category, metrics.TransferCancel, metrics.NoLabel))
|
||||
if err := m.ie.SendMetric(metrics.New(m.category, metrics.TransferCancel, metrics.NoLabel)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *metricsManager) Fail() {
|
||||
m.ie.SendMetric(metrics.New(m.category, metrics.TransferFail, metrics.NoLabel))
|
||||
if err := m.ie.SendMetric(metrics.New(m.category, metrics.TransferFail, metrics.NoLabel)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,11 +17,11 @@
|
||||
|
||||
package importexport
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/internal/users"
|
||||
type Locator interface {
|
||||
ProvideLogsPath() (string, error)
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type Configer interface {
|
||||
users.Configer
|
||||
|
||||
GetLogDir() string
|
||||
type Cacher interface {
|
||||
GetTransferDir() string
|
||||
}
|
||||
|
||||
187
internal/locations/locations.go
Normal file
187
internal/locations/locations.go
Normal file
@ -0,0 +1,187 @@
|
||||
// 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 locations implements a type that provides cross-platform access to
|
||||
// standard filesystem locations, including config, cache and log directories.
|
||||
package locations
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/files"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Locations provides cross-platform access to standard locations.
|
||||
// On linux:
|
||||
// - settings: ~/.config/protonmail/<app>
|
||||
// - logs: ~/.cache/protonmail/<app>/logs
|
||||
// - cache: ~/.cache/protonmail/<app>/cache
|
||||
// - updates: ~/.cache/protonmail/<app>/updates
|
||||
// - lockfile: ~/.cache/protonmail/<app>/<app>.lock
|
||||
type Locations struct {
|
||||
userConfig, userCache string
|
||||
configName string
|
||||
}
|
||||
|
||||
// New returns a new locations object.
|
||||
func New(provider Provider, configName string) *Locations {
|
||||
return &Locations{
|
||||
userConfig: provider.UserConfig(),
|
||||
userCache: provider.UserCache(),
|
||||
configName: configName,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLockFile returns the path to the lock file (e.g. ~/.cache/<company>/<app>/<app>.lock).
|
||||
func (l *Locations) GetLockFile() string {
|
||||
return filepath.Join(l.userCache, l.configName+".lock")
|
||||
}
|
||||
|
||||
// GetLicenseFilePath returns path to liense file.
|
||||
func (l *Locations) GetLicenseFilePath() string {
|
||||
path := l.getLicenseFilePath()
|
||||
logrus.WithField("path", path).Info("License file path")
|
||||
return path
|
||||
}
|
||||
|
||||
func (l *Locations) getLicenseFilePath() string {
|
||||
// User can install app to different location, or user can run it
|
||||
// directly from the package without installation, or it could be
|
||||
// automatically updated (app started from differenet location).
|
||||
// For all those cases, first let's check LICENSE next to the binary.
|
||||
path := filepath.Join(filepath.Dir(os.Args[0]), "LICENSE")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
appName := l.configName
|
||||
if l.configName == "importExport" {
|
||||
appName = "import-export"
|
||||
}
|
||||
// Most Linux distributions.
|
||||
path := "/usr/share/doc/protonmail/" + appName + "/LICENSE"
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
// Arch distributions.
|
||||
return "/usr/share/licenses/protonmail-" + appName + "/LICENSE"
|
||||
case "darwin": //nolint[goconst]
|
||||
path := filepath.Join(filepath.Dir(os.Args[0]), "..", "Resources", "LICENSE")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
appName := "ProtonMail Bridge.app"
|
||||
if l.configName == "importExport" {
|
||||
appName = "ProtonMail Import-Export.app"
|
||||
}
|
||||
return "/Applications/" + appName + "/Contents/Resources/LICENSE"
|
||||
case "windows":
|
||||
path := filepath.Join(filepath.Dir(os.Args[0]), "LICENSE.txt")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
// This should not happen, Windows should be handled by relative
|
||||
// location to the binary above. This is just fallback which may
|
||||
// or may not work, depends where user installed the app and how
|
||||
// user started the app.
|
||||
return filepath.FromSlash("C:/Program Files/Proton Technologies AG/ProtonMail Bridge/LICENSE.txt")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ProvideSettingsPath returns a location for user settings (e.g. ~/.config/<company>/<app>).
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideSettingsPath() (string, error) {
|
||||
if err := os.MkdirAll(l.getSettingsPath(), 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return l.getSettingsPath(), nil
|
||||
}
|
||||
|
||||
// ProvideLogsPath returns a location for user logs (e.g. ~/.cache/<company>/<app>/logs).
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideLogsPath() (string, error) {
|
||||
if err := os.MkdirAll(l.getLogsPath(), 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return l.getLogsPath(), nil
|
||||
}
|
||||
|
||||
// ProvideCachePath returns a location for user cache dirs (e.g. ~/.cache/<company>/<app>/cache).
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideCachePath() (string, error) {
|
||||
if err := os.MkdirAll(l.getCachePath(), 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return l.getCachePath(), nil
|
||||
}
|
||||
|
||||
// ProvideUpdatesPath returns a location for update files (e.g. ~/.cache/<company>/<app>/updates).
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideUpdatesPath() (string, error) {
|
||||
if err := os.MkdirAll(l.getUpdatesPath(), 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return l.getUpdatesPath(), nil
|
||||
}
|
||||
|
||||
func (l *Locations) getSettingsPath() string {
|
||||
return l.userConfig
|
||||
}
|
||||
|
||||
func (l *Locations) getLogsPath() string {
|
||||
return filepath.Join(l.userCache, "logs")
|
||||
}
|
||||
|
||||
func (l *Locations) getCachePath() string {
|
||||
return filepath.Join(l.userCache, "cache")
|
||||
}
|
||||
|
||||
func (l *Locations) getUpdatesPath() string {
|
||||
return filepath.Join(l.userCache, "updates")
|
||||
}
|
||||
|
||||
// Clear removes everything except the lock file.
|
||||
func (l *Locations) Clear() error {
|
||||
return files.Remove(
|
||||
l.getSettingsPath(),
|
||||
l.getLogsPath(),
|
||||
l.getCachePath(),
|
||||
l.getUpdatesPath(),
|
||||
).Do()
|
||||
}
|
||||
|
||||
// Clean removes any unexpected files from the app cache folder
|
||||
// while leaving files in the standard locations untouched.
|
||||
func (l *Locations) Clean() error {
|
||||
return files.Remove(l.userCache).Except(
|
||||
l.GetLockFile(),
|
||||
l.getLogsPath(),
|
||||
l.getCachePath(),
|
||||
l.getUpdatesPath(),
|
||||
).Do()
|
||||
}
|
||||
152
internal/locations/locations_test.go
Normal file
152
internal/locations/locations_test.go
Normal file
@ -0,0 +1,152 @@
|
||||
// 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 locations
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeAppDirs struct {
|
||||
configDir, cacheDir string
|
||||
}
|
||||
|
||||
func (dirs *fakeAppDirs) UserConfig() string {
|
||||
return dirs.configDir
|
||||
}
|
||||
|
||||
func (dirs *fakeAppDirs) UserCache() string {
|
||||
return dirs.cacheDir
|
||||
}
|
||||
|
||||
func TestClearRemovesEverythingExceptLockFile(t *testing.T) {
|
||||
l := newTestLocations(t)
|
||||
|
||||
assert.NoError(t, l.Clear())
|
||||
|
||||
assert.FileExists(t, l.GetLockFile())
|
||||
assert.NoDirExists(t, l.getSettingsPath())
|
||||
assert.NoDirExists(t, l.getLogsPath())
|
||||
assert.NoDirExists(t, l.getCachePath())
|
||||
assert.NoDirExists(t, l.getUpdatesPath())
|
||||
}
|
||||
|
||||
func TestCleanLeavesStandardLocationsUntouched(t *testing.T) {
|
||||
l := newTestLocations(t)
|
||||
|
||||
createFilesInDir(t, l.getLogsPath(),
|
||||
"log1.txt",
|
||||
"log2.txt",
|
||||
)
|
||||
|
||||
assert.NoError(t, l.Clean())
|
||||
|
||||
assert.FileExists(t, l.GetLockFile())
|
||||
assert.DirExists(t, l.getSettingsPath())
|
||||
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())
|
||||
}
|
||||
|
||||
func TestCleanRemovesUnexpectedFilesAndFolders(t *testing.T) {
|
||||
l := newTestLocations(t)
|
||||
|
||||
createFilesInDir(t, l.userCache,
|
||||
"unexpected1.txt",
|
||||
"dir1/unexpected2.txt",
|
||||
"dir1/unexpected3.txt",
|
||||
"dir2/unexpected4.txt",
|
||||
"dir3/dir4/unexpected5.txt",
|
||||
)
|
||||
|
||||
require.FileExists(t, filepath.Join(l.userCache, "unexpected1.txt"))
|
||||
require.FileExists(t, filepath.Join(l.userCache, "dir1", "unexpected2.txt"))
|
||||
require.FileExists(t, filepath.Join(l.userCache, "dir1", "unexpected3.txt"))
|
||||
require.FileExists(t, filepath.Join(l.userCache, "dir2", "unexpected4.txt"))
|
||||
require.FileExists(t, filepath.Join(l.userCache, "dir3", "dir4", "unexpected5.txt"))
|
||||
|
||||
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"))
|
||||
assert.NoFileExists(t, filepath.Join(l.userCache, "dir1", "unexpected2.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(l.userCache, "dir1", "unexpected3.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(l.userCache, "dir2", "unexpected4.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(l.userCache, "dir3", "dir4", "unexpected5.txt"))
|
||||
}
|
||||
|
||||
func newFakeAppDirs(t *testing.T) *fakeAppDirs {
|
||||
configDir, err := ioutil.TempDir("", "test-locations-config")
|
||||
require.NoError(t, err)
|
||||
|
||||
cacheDir, err := ioutil.TempDir("", "test-locations-cache")
|
||||
require.NoError(t, err)
|
||||
|
||||
return &fakeAppDirs{
|
||||
configDir: configDir,
|
||||
cacheDir: cacheDir,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
logs, err := l.ProvideLogsPath()
|
||||
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)
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func createFilesInDir(t *testing.T, dir string, files ...string) {
|
||||
for _, target := range files {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, target)), 0700))
|
||||
|
||||
f, err := os.Create(filepath.Join(dir, target))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
}
|
||||
}
|
||||
59
internal/locations/provider.go
Normal file
59
internal/locations/provider.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 locations
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Provider provides standard locations.
|
||||
type Provider interface {
|
||||
UserConfig() string
|
||||
UserCache() string
|
||||
}
|
||||
|
||||
// DefaultProvider is a locations provider using the system-default storage locations.
|
||||
type DefaultProvider struct {
|
||||
config, cache string
|
||||
}
|
||||
|
||||
func NewDefaultProvider(name string) (*DefaultProvider, error) {
|
||||
config, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cache, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DefaultProvider{
|
||||
config: filepath.Join(config, name),
|
||||
cache: filepath.Join(cache, name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *DefaultProvider) UserConfig() string {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func (p *DefaultProvider) UserCache() string {
|
||||
return p.cache
|
||||
}
|
||||
85
internal/logging/clear.go
Normal file
85
internal/logging/clear.go
Normal file
@ -0,0 +1,85 @@
|
||||
// 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 logging
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func clearLogs(logDir string, maxLogs int) error {
|
||||
files, err := ioutil.ReadDir(logDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var logsWithPrefix []string
|
||||
var crashesWithPrefix []string
|
||||
|
||||
for _, file := range files {
|
||||
if matchLogName(file.Name()) {
|
||||
if matchStackTraceName(file.Name()) {
|
||||
crashesWithPrefix = append(crashesWithPrefix, file.Name())
|
||||
} else {
|
||||
logsWithPrefix = append(logsWithPrefix, file.Name())
|
||||
}
|
||||
} else {
|
||||
// Older versions of Bridge stored logs in subfolders for each version.
|
||||
// That also has to be cleared and the functionality can be removed after some time.
|
||||
if file.IsDir() {
|
||||
if err := clearLogs(filepath.Join(logDir, file.Name()), maxLogs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
removeLog(logDir, file.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeOldLogs(logDir, logsWithPrefix, maxLogs)
|
||||
removeOldLogs(logDir, crashesWithPrefix, maxLogs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeOldLogs(logDir string, filenames []string, maxLogs int) {
|
||||
count := len(filenames)
|
||||
if count <= maxLogs {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Strings(filenames) // Sorted by timestamp: oldest first.
|
||||
for _, filename := range filenames[:count-maxLogs] {
|
||||
removeLog(logDir, filename)
|
||||
}
|
||||
}
|
||||
|
||||
func removeLog(logDir, filename string) {
|
||||
// We need to be sure to delete only log files.
|
||||
// Directory with logs can also contain other files.
|
||||
if !matchLogName(filename) {
|
||||
return
|
||||
}
|
||||
if err := os.RemoveAll(filepath.Join(logDir, filename)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove old logs")
|
||||
}
|
||||
}
|
||||
62
internal/logging/crash.go
Normal file
62
internal/logging/crash.go
Normal file
@ -0,0 +1,62 @@
|
||||
// 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 logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/crash"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func DumpStackTrace(logsPath string) crash.RecoveryAction {
|
||||
return func(r interface{}) error {
|
||||
file := filepath.Join(logsPath, getStackTraceName(constants.Version, constants.Revision))
|
||||
|
||||
f, err := os.OpenFile(file, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := f.WriteString(fmt.Sprintf("Recover: %v", r)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pprof.Lookup("goroutine").WriteTo(f, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.WithField("file", file).Warn("Saved crash report")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getStackTraceName(version, revision string) string {
|
||||
return fmt.Sprintf("v%v_%v_crash_%v.log", version, revision, time.Now().Unix())
|
||||
}
|
||||
|
||||
func matchStackTraceName(name string) bool {
|
||||
return regexp.MustCompile(`^v.*_crash_.*\.log$`).MatchString(name)
|
||||
}
|
||||
90
internal/logging/logging.go
Normal file
90
internal/logging/logging.go
Normal file
@ -0,0 +1,90 @@
|
||||
// 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 logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/writer"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxLogSize defines the maximum log size we should permit.
|
||||
// Zendesk has a file size limit of 20MB. When the last N log files are zipped,
|
||||
// it should fit under 20MB. So here we permit up to 10MB (most files are a few hundred kB).
|
||||
MaxLogSize = 10 * 2 << 20
|
||||
|
||||
// MaxLogs defines how many old log files should be kept.
|
||||
MaxLogs = 3
|
||||
)
|
||||
|
||||
func Init(logsPath string) error {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: time.StampMilli,
|
||||
})
|
||||
|
||||
rotator, err := NewRotator(MaxLogSize, func() (io.WriteCloser, error) {
|
||||
if err := clearLogs(logsPath, MaxLogs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return os.Create(filepath.Join(logsPath, getLogName(constants.Version, constants.Revision)))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.SetOutput(rotator)
|
||||
|
||||
logrus.AddHook(&writer.Hook{
|
||||
Writer: os.Stderr,
|
||||
LogLevels: []logrus.Level{
|
||||
logrus.PanicLevel,
|
||||
logrus.FatalLevel,
|
||||
logrus.ErrorLevel,
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetLevel(level string) {
|
||||
if lvl, err := logrus.ParseLevel(level); err == nil {
|
||||
logrus.SetLevel(lvl)
|
||||
}
|
||||
|
||||
if logrus.GetLevel() == logrus.DebugLevel || logrus.GetLevel() == logrus.TraceLevel {
|
||||
logrus.SetOutput(os.Stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func getLogName(version, revision string) string {
|
||||
return fmt.Sprintf("v%v_%v_%v.log", version, revision, time.Now().Unix())
|
||||
}
|
||||
|
||||
func matchLogName(name string) bool {
|
||||
return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
|
||||
}
|
||||
69
internal/logging/logging_test.go
Normal file
69
internal/logging/logging_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
// 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 logging
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestClearLogs tests that cearLogs removes only bridge old log files keeping last three of them.
|
||||
func TestClearLogs(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "clear-logs-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "other.log"), []byte("Hello"), 0755))
|
||||
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "v1_10.log"), []byte("Hello"), 0755))
|
||||
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "v1_11.log"), []byte("Hello"), 0755))
|
||||
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "v2_12.log"), []byte("Hello"), 0755))
|
||||
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "v2_13.log"), []byte("Hello"), 0755))
|
||||
|
||||
require.NoError(t, clearLogs(dir, 3))
|
||||
checkFileNames(t, dir, []string{
|
||||
"other.log",
|
||||
"v1_11.log",
|
||||
"v2_12.log",
|
||||
"v2_13.log",
|
||||
})
|
||||
}
|
||||
|
||||
func checkFileNames(t *testing.T, dir string, expectedFileNames []string) {
|
||||
fileNames := getFileNames(t, dir)
|
||||
require.Equal(t, expectedFileNames, fileNames)
|
||||
}
|
||||
|
||||
func getFileNames(t *testing.T, dir string) []string {
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
fileNames := []string{}
|
||||
for _, file := range files {
|
||||
fileNames = append(fileNames, file.Name())
|
||||
if file.IsDir() {
|
||||
subDir := filepath.Join(dir, file.Name())
|
||||
subFileNames := getFileNames(t, subDir)
|
||||
for _, subFileName := range subFileNames {
|
||||
fileNames = append(fileNames, file.Name()+"/"+subFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileNames
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user