We build too many walls and not enough bridges

This commit is contained in:
Jakub
2020-04-08 12:59:16 +02:00
commit 17f4d6097a
494 changed files with 62753 additions and 0 deletions

94
internal/api/api.go Normal file
View File

@ -0,0 +1,94 @@
// Copyright (c) 2020 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 api provides HTTP API of the Bridge.
//
// API endpoints:
// * /focus, see focusHandler
package api
import (
"crypto/tls"
"fmt"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"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"
)
var (
log = config.GetLogEntry("api") //nolint[gochecknoglobals]
)
type apiServer struct {
host string
pref *config.Preferences
tls *tls.Config
certPath string
keyPath string
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]
return &apiServer{
host: bridge.Host,
pref: pref,
tls: tls,
certPath: certPath,
keyPath: keyPath,
eventListener: eventListener,
}
}
// Starts the server.
func (api *apiServer) ListenAndServe() {
mux := http.NewServeMux()
mux.HandleFunc("/focus", wrapper(api, focusHandler))
addr := api.getAddress()
server := &http.Server{
Addr: addr,
Handler: mux,
TLSConfig: api.tls,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}
log.Info("API listening at ", addr)
if err := server.ListenAndServeTLS(api.certPath, api.keyPath); err != nil {
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
log.Error("API failed: ", err)
}
defer server.Close() //nolint[errcheck]
}
func (api *apiServer) getAddress() string {
port := api.pref.GetInt(preferences.APIPortKey)
newPort := ports.FindFreePortFrom(port)
if newPort != port {
api.pref.SetInt(preferences.APIPortKey, newPort)
}
return getAPIAddress(api.host, newPort)
}
func getAPIAddress(host string, port int) string {
return fmt.Sprintf("%s:%d", host, port)
}

51
internal/api/ctx.go Normal file
View File

@ -0,0 +1,51 @@
// Copyright (c) 2020 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 api
import (
"net/http"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
// httpHandler with Go's Response and Request.
type httpHandler func(http.ResponseWriter, *http.Request)
// handler with our context.
type handler func(handlerContext) error
type handlerContext struct {
req *http.Request
resp http.ResponseWriter
eventListener listener.Listener
}
func wrapper(api *apiServer, callback handler) httpHandler {
return func(w http.ResponseWriter, req *http.Request) {
ctx := handlerContext{
req: req,
resp: w,
eventListener: api.eventListener,
}
err := callback(ctx)
if err != nil {
log.Error("API callback of ", req.URL, " failed: ", err)
http.Error(w, err.Error(), 500)
}
}
}

55
internal/api/focus.go Normal file
View File

@ -0,0 +1,55 @@
// Copyright (c) 2020 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 api
import (
"crypto/tls"
"fmt"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
)
// focusHandler should be called from other instances (attempt to start bridge
// for the second time) to get focus in the currently running instance.
func focusHandler(ctx handlerContext) error {
log.Info("Focus from other instance")
ctx.eventListener.Emit(events.SecondInstanceEvent, "")
fmt.Fprintf(ctx.resp, "OK")
return nil
}
// CheckOtherInstanceAndFocus is helper for new instances to check if there is
// already a running instance and get it's focus.
func CheckOtherInstanceAndFocus(port int, tls *tls.Config) error {
transport := &http.Transport{TLSClientConfig: tls}
client := &http.Client{Transport: transport}
addr := getAPIAddress(bridge.Host, port)
resp, err := client.Get("https://" + addr + "/focus")
if err != nil {
return err
}
defer resp.Body.Close() //nolint[errcheck]
if resp.StatusCode != 200 {
log.Error("Focus error: ", resp.StatusCode)
}
return nil
}

510
internal/bridge/bridge.go Normal file
View File

@ -0,0 +1,510 @@
// Copyright (c) 2020 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 provides core business logic providing API over credentials store and PM API.
package bridge
import (
"errors"
"strconv"
"strings"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
m "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/hashicorp/go-multierror"
logrus "github.com/sirupsen/logrus"
)
var (
log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
isApplicationOutdated = false //nolint[gochecknoglobals]
)
// Bridge is a struct handling users.
type Bridge struct {
config Configer
pref PreferenceProvider
panicHandler PanicHandler
events listener.Listener
version string
pmapiClientFactory PMAPIProviderFactory
credStorer CredentialsStorer
storeCache *store.Cache
// users is a list of accounts that have been added to bridge.
// They are stored sorted in the credentials store in the order
// that they were added to bridge chronologically.
// People are used to that and so we preserve that ordering here.
users []*User
// idleUpdates is a channel which the imap backend listens to and which it uses
// to send idle updates to the mail client (eg thunderbird).
// The user stores should send idle updates on this channel.
idleUpdates chan interface{}
lock sync.RWMutex
userAgentClientName string
userAgentClientVersion string
userAgentOS string
}
func New(
config Configer,
pref PreferenceProvider,
panicHandler PanicHandler,
eventListener listener.Listener,
version string,
pmapiClientFactory PMAPIProviderFactory,
credStorer CredentialsStorer,
) *Bridge {
log.Trace("Creating new bridge")
b := &Bridge{
config: config,
pref: pref,
panicHandler: panicHandler,
events: eventListener,
version: version,
pmapiClientFactory: pmapiClientFactory,
credStorer: credStorer,
storeCache: store.NewCache(config.GetIMAPCachePath()),
idleUpdates: make(chan interface{}),
lock: sync.RWMutex{},
}
// Allow DoH before starting bridge if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if pref.GetBool(preferences.AllowProxyKey) {
AllowDoH()
}
go func() {
defer panicHandler.HandlePanic()
b.watchBridgeOutdated()
}()
if b.credStorer == nil {
log.Error("Bridge has no credentials store")
} else if err := b.loadUsersFromCredentialsStore(); err != nil {
log.WithError(err).Error("Could not load all users from credentials store")
}
if pref.GetBool(preferences.FirstStartKey) {
b.SendMetric(m.New(m.Setup, m.FirstStart, m.Label(version)))
}
go b.heartbeat()
return b
}
// heartbeat sends a heartbeat signal once a day.
func (b *Bridge) heartbeat() {
for range time.NewTicker(1 * time.Hour).C {
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
if err != nil {
continue
}
nextTime := time.Unix(next, 0)
if time.Now().After(nextTime) {
b.SendMetric(m.New(m.Heartbeat, m.Daily, m.NoLabel))
nextTime = nextTime.Add(24 * time.Hour)
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
}
}
}
func (b *Bridge) loadUsersFromCredentialsStore() (err error) {
b.lock.Lock()
defer b.lock.Unlock()
userIDs, err := b.credStorer.List()
if err != nil {
return
}
for _, userID := range userIDs {
l := log.WithField("user", userID)
apiClient := b.pmapiClientFactory(userID)
user, newUserErr := newUser(b.panicHandler, userID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
if newUserErr != nil {
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
continue
}
b.users = append(b.users, user)
if initUserErr := user.init(b.idleUpdates, apiClient); initUserErr != nil {
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
}
}
return err
}
func (b *Bridge) watchBridgeOutdated() {
ch := make(chan string)
b.events.Add(events.UpgradeApplicationEvent, ch)
for range ch {
isApplicationOutdated = true
b.closeAllConnections()
}
}
func (b *Bridge) closeAllConnections() {
for _, user := range b.users {
user.closeAllConnections()
}
}
// Login authenticates a user.
// The login flow:
// * Authenticate user:
// client, auth, err := bridge.Authenticate(username, password)
//
// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
// auth2FA, err := client.Auth2FA(twoFactorCode)
//
// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
// and then finish the login procedure.
// user, err := bridge.FinishLogin(client, auth, mailboxPassword)
func (b *Bridge) Login(username, password string) (loginClient PMAPIProvider, auth *pmapi.Auth, err error) {
log.WithField("username", username).Trace("Logging in to bridge")
b.crashBandicoot(username)
// We need to use "login" client because we need userID to properly
// assign access tokens into token manager.
loginClient = b.pmapiClientFactory("login")
authInfo, err := loginClient.AuthInfo(username)
if err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
return nil, nil, err
}
if auth, err = loginClient.Auth(username, password, authInfo); err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth for user")
return loginClient, auth, err
}
return loginClient, auth, nil
}
// FinishLogin finishes the login procedure and adds the user into the credentials store.
// See `Login` for more details of the login flow.
func (b *Bridge) FinishLogin(loginClient PMAPIProvider, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
log.Trace("Finishing bridge login")
defer func() {
if err == pmapi.ErrUpgradeApplication {
b.events.Emit(events.UpgradeApplicationEvent, "")
}
}()
b.lock.Lock()
defer b.lock.Unlock()
mbPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
if err != nil {
log.WithError(err).Error("Could not hash mailbox password")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after hash password failed.")
}
return
}
if _, err = loginClient.Unlock(mbPassword); err != nil {
log.WithError(err).Error("Could not decrypt keyring")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after unlock failed.")
}
return
}
apiUser, err := loginClient.CurrentUser()
if err != nil {
log.WithError(err).Error("Could not get login API user")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
}
return
}
user, hasUser := b.hasUser(apiUser.ID)
// If the user exists and is logged in, we don't want to do anything.
if hasUser && user.IsConnected() {
err = errors.New("user is already logged in")
log.WithError(err).Warn("User is already logged in")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Warn("Could not discard auth generated during second login")
}
return
}
apiToken := auth.UID() + ":" + auth.RefreshToken
apiClient := b.pmapiClientFactory(apiUser.ID)
auth, err = apiClient.AuthRefresh(apiToken)
if err != nil {
log.WithError(err).Error("Could refresh token in new client")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Warn("Could not discard auth generated after auth refresh")
}
return
}
// We load the current user again because it should now have addresses loaded.
apiUser, err = apiClient.CurrentUser()
if err != nil {
log.WithError(err).Error("Could not get current API user")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
}
return
}
apiToken = auth.UID() + ":" + auth.RefreshToken
activeEmails := apiClient.Addresses().ActiveEmails()
if _, err = b.credStorer.Add(apiUser.ID, apiUser.Name, apiToken, mbPassword, activeEmails); err != nil {
log.WithError(err).Error("Could not add user to credentials store")
return
}
// If it's a new user, generate the user object.
if !hasUser {
user, err = newUser(b.panicHandler, apiUser.ID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
if err != nil {
log.WithField("user", apiUser.ID).WithError(err).Error("Could not create user")
return
}
}
// Set up the user auth and store (which we do for both new and existing users).
if err = user.init(b.idleUpdates, apiClient); err != nil {
log.WithField("user", user.userID).WithError(err).Error("Could not initialise user")
return
}
if !hasUser {
b.users = append(b.users, user)
b.SendMetric(m.New(m.Setup, m.NewUser, m.NoLabel))
}
b.events.Emit(events.UserRefreshEvent, apiUser.ID)
return user, err
}
// GetUsers returns all added users into keychain (even logged out users).
func (b *Bridge) GetUsers() []*User {
b.lock.RLock()
defer b.lock.RUnlock()
return b.users
}
// GetUser returns a user by `query` which is compared to users' ID, username
// or any attached e-mail address.
func (b *Bridge) GetUser(query string) (*User, error) {
b.crashBandicoot(query)
b.lock.RLock()
defer b.lock.RUnlock()
for _, user := range b.users {
if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
return user, nil
}
for _, address := range user.GetAddresses() {
if strings.EqualFold(address, query) {
return user, nil
}
}
}
return nil, errors.New("user " + query + " not found")
}
// ClearData closes all connections (to release db files and so on) and clears all data.
func (b *Bridge) ClearData() error {
var result *multierror.Error
for _, user := range b.users {
if err := user.Logout(); err != nil {
result = multierror.Append(result, err)
}
if err := user.closeStore(); err != nil {
result = multierror.Append(result, err)
}
}
if err := b.config.ClearData(); err != nil {
result = multierror.Append(result, err)
}
return result.ErrorOrNil()
}
// DeleteUser deletes user completely; it logs user out from the API, stops any
// active connection, deletes from credentials store and removes from the Bridge struct.
func (b *Bridge) DeleteUser(userID string, clearStore bool) error {
b.lock.Lock()
defer b.lock.Unlock()
log := log.WithField("user", userID)
for idx, user := range b.users {
if user.ID() == userID {
if err := user.Logout(); err != nil {
log.WithError(err).Error("Cannot logout user")
// We can try to continue to remove the user.
// Token will still be valid, but will expire eventually.
}
if err := user.closeStore(); err != nil {
log.WithError(err).Error("Failed to close user store")
}
if clearStore {
// Clear cache after closing connections (done in logout).
if err := user.clearStore(); err != nil {
log.WithError(err).Error("Failed to clear user")
}
}
if err := b.credStorer.Delete(userID); err != nil {
log.WithError(err).Error("Cannot remove user")
return err
}
b.users = append(b.users[:idx], b.users[idx+1:]...)
return nil
}
}
return errors.New("user " + userID + " not found")
}
// ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
apiClient := b.pmapiClientFactory("bug_reporter")
title := "[Bridge] Bug"
err := apiClient.ReportBugWithEmailClient(
osType,
osVersion,
title,
description,
accountName,
address,
emailClient,
)
if err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
}
// SendMetric sends a metric. We don't want to return any errors, only log them.
func (b *Bridge) SendMetric(m m.Metric) {
apiClient := b.pmapiClientFactory("metric_reporter")
cat, act, lab := m.Get()
err := apiClient.SendSimpleMetric(string(cat), string(act), string(lab))
if err != nil {
log.Error("Sending metric failed: ", err)
}
log.WithFields(logrus.Fields{
"cat": cat,
"act": act,
"lab": lab,
}).Debug("Metric successfully sent")
}
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
func (b *Bridge) GetCurrentClient() string {
res := b.userAgentClientName
if b.userAgentClientVersion != "" {
res = res + " " + b.userAgentClientVersion
}
return res
}
// SetCurrentClient updates client info (e.g. Thunderbird) and sets the user agent
// on pmapi. By default no client is used, IMAP has to detect it on first login.
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
b.userAgentClientName = clientName
b.userAgentClientVersion = clientVersion
b.updateCurrentUserAgent()
}
// SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
// `runtime.GOOS`, but this can be overridden in case of better detection.
func (b *Bridge) SetCurrentOS(os string) {
b.userAgentOS = os
b.updateCurrentUserAgent()
}
// GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
func (b *Bridge) GetIMAPUpdatesChannel() chan interface{} {
if b.idleUpdates == nil {
log.Warn("Bridge updates channel is nil")
}
return b.idleUpdates
}
// AllowDoH instructs bridge to use DoH to access an API proxy if necessary.
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func AllowDoH() {
pmapi.GlobalAllowDoH()
}
// DisallowDoH instructs bridge to not use DoH to access an API proxy if necessary.
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func DisallowDoH() {
pmapi.GlobalDisallowDoH()
}
func (b *Bridge) updateCurrentUserAgent() {
UpdateCurrentUserAgent(b.version, b.userAgentOS, b.userAgentClientName, b.userAgentClientVersion)
}
// hasUser returns whether the bridge currently has a user with ID `id`.
func (b *Bridge) hasUser(id string) (user *User, ok bool) {
for _, u := range b.users {
if u.ID() == id {
user, ok = u, true
return
}
}
return
}
// "Easter egg" for testing purposes.
func (b *Bridge) crashBandicoot(username string) {
if username == "crash@bandicoot" {
panic("Your wish is my command… I crash!")
}
}

View File

@ -0,0 +1,233 @@
// Copyright (c) 2020 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
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestBridgeFinishLoginBadPassword(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Init bridge with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Set up mocks for FinishLogin.
err := errors.New("bad password")
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, err)
m.pmapiClient.EXPECT().Logout().Return(nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
}
func TestBridgeFinishLoginUpgradeApplication(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Init bridge with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Set up mocks for FinishLogin.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, pmapi.ErrUpgradeApplication)
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
err := errors.New("Cannot logout when upgrade needed")
m.pmapiClient.EXPECT().Logout().Return(err)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
}
func refreshWithToken(token string) *pmapi.Auth {
return &pmapi.Auth{
RefreshToken: token,
KeySalt: "", // No salting in tests.
}
}
func credentialsWithToken(token string) *credentials.Credentials {
tmp := &credentials.Credentials{}
*tmp = *testCredentials
tmp.APIToken = token
return tmp
}
func TestBridgeFinishLoginNewUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Bridge finds no users in the keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Get user to be able to setup new client with proper userID.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
// Setup of new client.
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
// Set up mocks for authorising the new user (in user.init).
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil).Times(2)
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
// Set up mocks for creating the user's store (in store.New).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
// Emit event for new user and send metrics.
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.NewUser), string(metrics.NoLabel))
// Set up mocks for starting the store's event loop (in store.New).
// The event loop runs in another goroutine so this might happen at any time.
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Set up mocks for performing the initial store sync.
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
}
func TestBridgeFinishLoginExistingUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
loggedOutCreds := *testCredentials
loggedOutCreds.APIToken = ""
loggedOutCreds.MailboxPassword = ""
// Bridge finds one logged out user in the keychain.
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
// New user
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
// Init user
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrInvalidToken)
m.pmapiClient.EXPECT().Addresses().Return(nil)
// Get user to be able to setup new client with proper userID.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
// Setup of new client.
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
// Set up mocks for authorising the new user (in user.init).
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil)
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
// Set up mocks for creating the user's store (in store.New).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
// Reload account list in GUI.
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
// Set up mocks for starting the store's event loop (in store.New)
// The event loop runs in another goroutine so this might happen at any time.
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Set up mocks for performing the initial store sync.
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
}
func TestBridgeDoubleLogin(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Firstly, start bridge with existing user...
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
// Then, try to log in again...
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Logout()
_, err := bridge.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
assert.Equal(t, "user is already logged in", err.Error())
}
func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) {
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
user, err := bridge.FinishLogin(m.pmapiClient, auth, mailboxPassword)
waitForEvents()
assert.Equal(t, expectedErr, err)
if expectedUserID != "" {
assert.Equal(t, expectedUserID, user.ID())
assert.Equal(t, 1, len(bridge.users))
assert.Equal(t, expectedUserID, bridge.users[0].ID())
} else {
assert.Equal(t, (*User)(nil), user)
assert.Equal(t, 0, len(bridge.users))
}
}

View File

@ -0,0 +1,162 @@
// Copyright (c) 2020 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
import (
"errors"
"testing"
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestNewBridgeNoKeychain(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain"))
checkBridgeNew(t, m, []*credentials.Credentials{})
}
func TestNewBridgeWithoutUsersInCredentialsStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
checkBridgeNew(t, m, []*credentials.Credentials{})
}
func TestNewBridgeWithDisconnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
m.pmapiClient.EXPECT().Addresses().Return(nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
}
func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
}
func TestNewBridgeWithConnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
// Set up mocks for store initialisation for the authorized user.
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentials})
}
// Tests two users with different states and checks also the order from
// credentials store is kept also in array of Bridge users.
func TestNewBridgeWithUsers(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().List().Return([]string{"user", "user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
// Set up mocks for store initialisation for the unauth user.
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
m.pmapiClient.EXPECT().Addresses().Return(nil)
// Set up mocks for store initialisation for the authorized user.
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
}
func TestNewBridgeFirstStart(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(true)
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.FirstStart), gomock.Any())
testNewBridge(t, m)
}
func checkBridgeNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
assert.Equal(m.t, len(expectedCredentials), len(bridge.GetUsers()))
credentials := []*credentials.Credentials{}
for _, user := range bridge.users {
credentials = append(credentials, user.creds)
}
assert.Equal(m.t, expectedCredentials, credentials)
}

View File

@ -0,0 +1,256 @@
// Copyright (c) 2020 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
import (
"io/ioutil"
"os"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
bridgemocks "github.com/ProtonMail/proton-bridge/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
if os.Getenv("VERBOSITY") == "trace" {
logrus.SetLevel(logrus.TraceLevel)
}
os.Exit(m.Run())
}
var (
testAuth = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "tok",
KeySalt: "", // No salting in tests.
}
testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "reftok",
KeySalt: "", // No salting in tests.
}
testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals]
UserID: "user",
Name: "username",
Emails: "user@pm.me",
APIToken: "token",
MailboxPassword: "pass",
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
IsHidden: false,
IsCombinedAddressMode: true,
}
testCredentialsSplit = &credentials.Credentials{ //nolint[gochecknoglobals]
UserID: "users",
Name: "usersname",
Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me",
APIToken: "token",
MailboxPassword: "pass",
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
IsHidden: false,
IsCombinedAddressMode: false,
}
testCredentialsDisconnected = &credentials.Credentials{ //nolint[gochecknoglobals]
UserID: "user",
Name: "username",
Emails: "user@pm.me",
APIToken: "",
MailboxPassword: "",
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
IsHidden: false,
IsCombinedAddressMode: true,
}
testPMAPIUser = &pmapi.User{ //nolint[gochecknoglobals]
ID: "user",
Name: "username",
}
testPMAPIAddress = &pmapi.Address{ //nolint[gochecknoglobals]
ID: "testAddressID",
Type: pmapi.OriginalAddress,
Email: "user@pm.me",
Receive: pmapi.CanReceive,
}
testPMAPIAddresses = []*pmapi.Address{ //nolint[gochecknoglobals]
{ID: "usersAddress1ID", Email: "users@pm.me", Receive: pmapi.CanReceive, Type: pmapi.OriginalAddress},
{ID: "usersAddress2ID", Email: "anotheruser@pm.me", Receive: pmapi.CanReceive, Type: pmapi.AliasAddress},
{ID: "usersAddress3ID", Email: "alsouser@pm.me", Receive: pmapi.CanReceive, Type: pmapi.AliasAddress},
}
testPMAPIEvent = &pmapi.Event{ // nolint[gochecknoglobals]
EventID: "ACXDmTaBub14w==",
}
)
func waitForEvents() {
// Wait for goroutine to add listener.
// E.g. calling login to invoke firstsync event. Functions can end sooner than
// goroutines call the listener mock. We need to wait a little bit before the end of
// the test to capture all event calls. This allows us to detect whether there were
// missing calls, or perhaps whether something was called too many times.
time.Sleep(100 * time.Millisecond)
}
type mocks struct {
t *testing.T
ctrl *gomock.Controller
config *bridgemocks.MockConfiger
PanicHandler *bridgemocks.MockPanicHandler
prefProvider *bridgemocks.MockPreferenceProvider
pmapiClient *bridgemocks.MockPMAPIProvider
credentialsStore *bridgemocks.MockCredentialsStorer
eventListener *MockListener
storeCache *store.Cache
}
func initMocks(t *testing.T) mocks {
mockCtrl := gomock.NewController(t)
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache")
m := mocks{
t: t,
ctrl: mockCtrl,
config: bridgemocks.NewMockConfiger(mockCtrl),
PanicHandler: bridgemocks.NewMockPanicHandler(mockCtrl),
pmapiClient: bridgemocks.NewMockPMAPIProvider(mockCtrl),
prefProvider: bridgemocks.NewMockPreferenceProvider(mockCtrl),
credentialsStore: bridgemocks.NewMockCredentialsStorer(mockCtrl),
eventListener: NewMockListener(mockCtrl),
storeCache: store.NewCache(cacheFile.Name()),
}
// Ignore heartbeat calls because they always happen.
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Heartbeat), gomock.Any(), gomock.Any()).AnyTimes()
m.prefProvider.EXPECT().Get(preferences.NextHeartbeatKey).AnyTimes()
m.prefProvider.EXPECT().Set(preferences.NextHeartbeatKey, gomock.Any()).AnyTimes()
// Called during clean-up.
m.PanicHandler.EXPECT().HandlePanic().AnyTimes()
return m
}
func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge {
// Init for user.
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Init for users.
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("users", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil)
return testNewBridge(t, m)
}
func testNewBridge(t *testing.T, m mocks) *Bridge {
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache")
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(false).AnyTimes()
m.prefProvider.EXPECT().GetBool(preferences.AllowProxyKey).Return(false).AnyTimes()
m.config.EXPECT().GetDBDir().Return("/tmp").AnyTimes()
m.config.EXPECT().GetIMAPCachePath().Return(cacheFile.Name()).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any()).AnyTimes()
m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any())
pmapiClientFactory := func(userID string) PMAPIProvider {
log.WithField("userID", userID).Info("Creating new pmclient")
return m.pmapiClient
}
bridge := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, "ver", pmapiClientFactory, m.credentialsStore)
waitForEvents()
return bridge
}
func cleanUpBridgeUserData(b *Bridge) {
for _, user := range b.users {
_ = user.clearStore()
}
}
func TestClearData(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "alsouser@pm.me")
m.pmapiClient.EXPECT().Logout()
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Logout()
m.credentialsStore.EXPECT().Logout("users").Return(nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil)
m.config.EXPECT().ClearData().Return(nil)
require.NoError(t, bridge.ClearData())
waitForEvents()
}

View File

@ -0,0 +1,121 @@
// Copyright (c) 2020 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
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/stretchr/testify/assert"
)
func TestGetNoUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "nouser", -1, "user nouser not found")
}
func TestGetUserByID(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "user", 0, "")
checkBridgeGetUser(t, m, "users", 1, "")
}
func TestGetUserByName(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "username", 0, "")
checkBridgeGetUser(t, m, "usersname", 1, "")
}
func TestGetUserByEmail(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "user@pm.me", 0, "")
checkBridgeGetUser(t, m, "users@pm.me", 1, "")
checkBridgeGetUser(t, m, "anotheruser@pm.me", 1, "")
checkBridgeGetUser(t, m, "alsouser@pm.me", 1, "")
}
func TestDeleteUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Delete("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := bridge.DeleteUser("user", true)
assert.NoError(t, err)
assert.Equal(t, 1, len(bridge.users))
}
// Even when logout fails, delete is done.
func TestDeleteUserWithFailingLogout(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed"))
m.credentialsStore.EXPECT().Delete("user").Return(nil).Times(2)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := bridge.DeleteUser("user", true)
assert.NoError(t, err)
assert.Equal(t, 1, len(bridge.users))
}
func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
user, err := bridge.GetUser(query)
waitForEvents()
if expectedError != "" {
assert.Equal(m.t, expectedError, err.Error())
} else {
assert.NoError(m.t, err)
}
var expectedUser *User
if index >= 0 {
expectedUser = bridge.users[index]
}
assert.Equal(m.t, expectedUser, user)
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2020 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
// Host settings.
const (
Host = "127.0.0.1"
)

View File

@ -0,0 +1,137 @@
// Copyright (c) 2020 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 credentials implements our struct stored in keychain.
// Store struct is kind of like a database client.
// Credentials struct is kind of like one record from the database.
package credentials
import (
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/sirupsen/logrus"
)
const sep = "\x00"
var (
log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
ErrWrongFormat = errors.New("backend/creds: malformed password")
)
type Credentials struct {
UserID, // Do not marshal; used as a key.
Name,
Emails,
APIToken,
MailboxPassword,
BridgePassword,
Version string
Timestamp int64
IsHidden, // Deprecated.
IsCombinedAddressMode bool
}
func (s *Credentials) Marshal() string {
items := []string{
s.Name, // 0
s.Emails, // 1
s.APIToken, // 2
s.MailboxPassword, // 3
s.BridgePassword, // 4
s.Version, // 5
"", // 6
"", // 7
"", // 8
}
items[6] = fmt.Sprint(s.Timestamp)
if s.IsHidden {
items[7] = "1"
}
if s.IsCombinedAddressMode {
items[8] = "1"
}
str := strings.Join(items, sep)
return base64.StdEncoding.EncodeToString([]byte(str))
}
func (s *Credentials) Unmarshal(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
items := strings.Split(string(b), sep)
if len(items) != 9 {
return ErrWrongFormat
}
s.Name = items[0]
s.Emails = items[1]
s.APIToken = items[2]
s.MailboxPassword = items[3]
s.BridgePassword = items[4]
s.Version = items[5]
if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil {
s.Timestamp = 0
}
if s.IsHidden = false; items[7] == "1" {
s.IsHidden = true
}
if s.IsCombinedAddressMode = false; items[8] == "1" {
s.IsCombinedAddressMode = true
}
return nil
}
func (s *Credentials) SetEmailList(list []string) {
s.Emails = strings.Join(list, ";")
}
func (s *Credentials) EmailList() []string {
return strings.Split(s.Emails, ";")
}
func (s *Credentials) CheckPassword(password string) error {
if subtle.ConstantTimeCompare([]byte(s.BridgePassword), []byte(password)) != 1 {
log.WithFields(logrus.Fields{
"userID": s.UserID,
}).Debug("Incorrect bridge password")
return fmt.Errorf("backend/credentials: incorrect password")
}
return nil
}
func (s *Credentials) Logout() {
s.APIToken = ""
s.MailboxPassword = ""
}
func (s *Credentials) IsConnected() bool {
return s.APIToken != "" && s.MailboxPassword != ""
}

View File

@ -0,0 +1,39 @@
// Copyright (c) 2020 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 credentials
import (
"crypto/rand"
"encoding/base64"
"io"
)
const keySize = 16
// generateKey generates a new random key.
func generateKey() []byte {
key := make([]byte, keySize)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
panic(err)
}
return key
}
func generatePassword() string {
return base64.RawURLEncoding.EncodeToString(generateKey())
}

View File

@ -0,0 +1,316 @@
// Copyright (c) 2020 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 credentials
import (
"errors"
"fmt"
"sort"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/sirupsen/logrus"
)
var storeLocker = sync.RWMutex{} //nolint[gochecknoglobals]
// Store is an encrypted credentials store.
type Store struct {
secrets *keychain.Access
}
// NewStore creates a new encrypted credentials store.
func NewStore() (*Store, error) {
secrets, err := keychain.NewAccess("bridge")
return &Store{
secrets: secrets,
}, err
}
func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails []string) (creds *Credentials, err error) {
storeLocker.Lock()
defer storeLocker.Unlock()
log.WithFields(logrus.Fields{
"user": userID,
"username": userName,
"emails": emails,
}).Trace("Adding new credentials")
if err = s.checkKeychain(); err != nil {
return
}
creds = &Credentials{
UserID: userID,
Name: userName,
APIToken: apiToken,
MailboxPassword: mailboxPassword,
IsHidden: false,
}
creds.SetEmailList(emails)
var has bool
if has, err = s.has(userID); err != nil {
log.WithField("userID", userID).WithError(err).Error("Could not check if user credentials already exist")
return
}
if has {
log.Info("Updating credentials of existing user")
currentCredentials, err := s.get(userID)
if err != nil {
return nil, err
}
creds.BridgePassword = currentCredentials.BridgePassword
creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode
creds.Timestamp = currentCredentials.Timestamp
} else {
log.Info("Generating credentials for new user")
creds.BridgePassword = generatePassword()
creds.IsCombinedAddressMode = true
creds.Timestamp = time.Now().Unix()
}
if err = s.saveCredentials(creds); err != nil {
return
}
return creds, err
}
func (s *Store) SwitchAddressMode(userID string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.IsCombinedAddressMode = !credentials.IsCombinedAddressMode
credentials.BridgePassword = generatePassword()
return s.saveCredentials(credentials)
}
func (s *Store) UpdateEmails(userID string, emails []string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.SetEmailList(emails)
return s.saveCredentials(credentials)
}
func (s *Store) UpdateToken(userID, apiToken string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.APIToken = apiToken
return s.saveCredentials(credentials)
}
func (s *Store) Logout(userID string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.Logout()
return s.saveCredentials(credentials)
}
// List returns a list of usernames that have credentials stored.
func (s *Store) List() (userIDs []string, err error) {
storeLocker.RLock()
defer storeLocker.RUnlock()
log.Trace("Listing credentials in credentials store")
if err = s.checkKeychain(); err != nil {
return
}
var allUserIDs []string
if allUserIDs, err = s.secrets.List(); err != nil {
log.WithError(err).Error("Could not list credentials")
return
}
credentialList := []*Credentials{}
for _, userID := range allUserIDs {
creds, getErr := s.get(userID)
if getErr != nil {
log.WithField("userID", userID).WithError(getErr).Warn("Failed to get credentials")
continue
}
if creds.Timestamp == 0 {
continue
}
credentialList = append(credentialList, creds)
}
sort.Slice(credentialList, func(i, j int) bool {
return credentialList[i].Timestamp < credentialList[j].Timestamp
})
for _, credentials := range credentialList {
userIDs = append(userIDs, credentials.UserID)
}
return userIDs, err
}
func (s *Store) GetAndCheckPassword(userID, password string) (creds *Credentials, err error) {
storeLocker.RLock()
defer storeLocker.RUnlock()
log.WithFields(logrus.Fields{
"userID": userID,
}).Debug("Checking bridge password")
credentials, err := s.Get(userID)
if err != nil {
return nil, err
}
if err := credentials.CheckPassword(password); err != nil {
log.WithFields(logrus.Fields{
"userID": userID,
"err": err,
}).Debug("Incorrect bridge password")
return nil, err
}
return credentials, nil
}
func (s *Store) Get(userID string) (creds *Credentials, err error) {
storeLocker.RLock()
defer storeLocker.RUnlock()
var has bool
if has, err = s.has(userID); err != nil {
log.WithError(err).Error("Could not check for credentials")
return
}
if !has {
err = errors.New("no credentials found for given userID")
return
}
return s.get(userID)
}
func (s *Store) has(userID string) (has bool, err error) {
if err = s.checkKeychain(); err != nil {
return
}
var ids []string
if ids, err = s.secrets.List(); err != nil {
log.WithError(err).Error("Could not list credentials")
return
}
for _, id := range ids {
if id == userID {
has = true
}
}
return
}
func (s *Store) get(userID string) (creds *Credentials, err error) {
log := log.WithField("user", userID)
if err = s.checkKeychain(); err != nil {
return
}
secret, err := s.secrets.Get(userID)
if err != nil {
log.WithError(err).Error("Could not get credentials from native keychain")
return
}
credentials := &Credentials{UserID: userID}
if err = credentials.Unmarshal(secret); err != nil {
err = fmt.Errorf("backend/credentials: malformed secret: %v", err)
_ = s.secrets.Delete(userID)
log.WithError(err).Error("Could not unmarshal secret")
return
}
return credentials, nil
}
// saveCredentials encrypts and saves password to the keychain store.
func (s *Store) saveCredentials(credentials *Credentials) (err error) {
if err = s.checkKeychain(); err != nil {
return
}
credentials.Version = keychain.KeychainVersion
return s.secrets.Put(credentials.UserID, credentials.Marshal())
}
func (s *Store) checkKeychain() (err error) {
if s.secrets == nil {
err = keychain.ErrNoKeychainInstalled
log.WithError(err).Error("Store is unusable")
}
return
}
// Delete removes credentials from the store.
func (s *Store) Delete(userID string) (err error) {
storeLocker.Lock()
defer storeLocker.Unlock()
if err = s.checkKeychain(); err != nil {
return
}
return s.secrets.Delete(userID)
}

View File

@ -0,0 +1,297 @@
// Copyright (c) 2020 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 credentials
import (
"bytes"
"encoding/base64"
"encoding/gob"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testSep = "\n"
const secretFormat = "%v" + testSep + // UserID,
"%v" + testSep + // Name,
"%v" + testSep + // Emails,
"%v" + testSep + // APIToken,
"%v" + testSep + // Mailbox,
"%v" + testSep + // BridgePassword,
"%v" + testSep + // Version string
"%v" + testSep + // Timestamp,
"%v" + testSep + // IsHidden,
"%v" // IsCombinedAddressMode
// the best would be to run this test on mac, win, and linux separately
type testCredentials struct {
UserID,
Name,
Emails,
APIToken,
Mailbox,
BridgePassword,
Version string
Timestamp int64
IsHidden,
IsCombinedAddressMode bool
}
func init() { //nolint[gochecknoinits]
gob.Register(testCredentials{})
}
func (s *testCredentials) MarshalGob() string {
buf := bytes.Buffer{}
enc := gob.NewEncoder(&buf)
if err := enc.Encode(s); err != nil {
return ""
}
fmt.Printf("MarshalGob: %#v\n", buf.String())
return base64.StdEncoding.EncodeToString(buf.Bytes())
}
func (s *testCredentials) Clear() {
s.UserID = ""
s.Name = ""
s.Emails = ""
s.APIToken = ""
s.Mailbox = ""
s.BridgePassword = ""
s.Version = ""
s.Timestamp = 0
s.IsHidden = false
s.IsCombinedAddressMode = false
}
func (s *testCredentials) UnmarshalGob(secret string) error {
s.Clear()
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
fmt.Println("decode base64", b)
return err
}
buf := bytes.NewBuffer(b)
dec := gob.NewDecoder(buf)
if err = dec.Decode(s); err != nil {
fmt.Println("decode gob", b, buf.Bytes())
return err
}
return nil
}
func (s *testCredentials) ToJSON() string {
if b, err := json.Marshal(s); err == nil {
fmt.Printf("MarshalJSON: %#v\n", string(b))
return base64.StdEncoding.EncodeToString(b)
}
return ""
}
func (s *testCredentials) FromJSON(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
if err = json.Unmarshal(b, s); err == nil {
return nil
}
return err
}
func (s *testCredentials) MarshalFmt() string {
buf := bytes.Buffer{}
fmt.Fprintf(
&buf, secretFormat,
s.UserID,
s.Name,
s.Emails,
s.APIToken,
s.Mailbox,
s.BridgePassword,
s.Version,
s.Timestamp,
s.IsHidden,
s.IsCombinedAddressMode,
)
fmt.Printf("MarshalFmt: %#v\n", buf.String())
return base64.StdEncoding.EncodeToString(buf.Bytes())
}
func (s *testCredentials) UnmarshalFmt(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
buf := bytes.NewBuffer(b)
fmt.Println("decode fmt", b, buf.Bytes())
_, err = fmt.Fscanf(
buf, secretFormat,
&s.UserID,
&s.Name,
&s.Emails,
&s.APIToken,
&s.Mailbox,
&s.BridgePassword,
&s.Version,
&s.Timestamp,
&s.IsHidden,
&s.IsCombinedAddressMode,
)
if err != nil {
return err
}
return nil
}
func (s *testCredentials) MarshalStrings() string { // this is the most space efficient
items := []string{
s.UserID, // 0
s.Name, // 1
s.Emails, // 2
s.APIToken, // 3
s.Mailbox, // 4
s.BridgePassword, // 5
s.Version, // 6
}
items = append(items, fmt.Sprint(s.Timestamp)) // 7
if s.IsHidden { // 8
items = append(items, "1")
} else {
items = append(items, "")
}
if s.IsCombinedAddressMode { // 9
items = append(items, "1")
} else {
items = append(items, "")
}
str := strings.Join(items, sep)
fmt.Printf("MarshalJoin: %#v\n", str)
return base64.StdEncoding.EncodeToString([]byte(str))
}
func (s *testCredentials) UnmarshalStrings(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
items := strings.Split(string(b), sep)
if len(items) != 10 {
return ErrWrongFormat
}
s.UserID = items[0]
s.Name = items[1]
s.Emails = items[2]
s.APIToken = items[3]
s.Mailbox = items[4]
s.BridgePassword = items[5]
s.Version = items[6]
if _, err = fmt.Sscanf(items[7], "%d", &s.Timestamp); err != nil {
s.Timestamp = 0
}
if s.IsHidden = false; items[8] == "1" {
s.IsHidden = true
}
if s.IsCombinedAddressMode = false; items[9] == "1" {
s.IsCombinedAddressMode = true
}
return nil
}
func (s *testCredentials) IsSame(rhs *testCredentials) bool {
return s.Name == rhs.Name &&
s.Emails == rhs.Emails &&
s.APIToken == rhs.APIToken &&
s.Mailbox == rhs.Mailbox &&
s.BridgePassword == rhs.BridgePassword &&
s.Version == rhs.Version &&
s.Timestamp == rhs.Timestamp &&
s.IsHidden == rhs.IsHidden &&
s.IsCombinedAddressMode == rhs.IsCombinedAddressMode
}
func TestMarshalFormats(t *testing.T) {
input := testCredentials{UserID: "007", Emails: "ja@pm.me;jakub@cu.th", Timestamp: 152469263742, IsHidden: true}
fmt.Printf("input %#v\n", input)
secretStrings := input.MarshalStrings()
fmt.Printf("secretStrings %#v %d\n", secretStrings, len(secretStrings))
secretGob := input.MarshalGob()
fmt.Printf("secretGob %#v %d\n", secretGob, len(secretGob))
secretJSON := input.ToJSON()
fmt.Printf("secretJSON %#v %d\n", secretJSON, len(secretJSON))
secretFmt := input.MarshalFmt()
fmt.Printf("secretFmt %#v %d\n", secretFmt, len(secretFmt))
output := testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalStrings(secretStrings))
fmt.Printf("strings out %#v \n", output)
require.True(t, input.IsSame(&output), "strings out not same")
output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalGob(secretGob))
fmt.Printf("gob out %#v\n \n", output)
assert.Equal(t, input, output)
output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.FromJSON(secretJSON))
fmt.Printf("json out %#v \n", output)
require.True(t, input.IsSame(&output), "json out not same")
/*
// Simple Fscanf not working!
output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalFmt(secretFmt))
fmt.Printf("fmt out %#v \n", output)
require.True(t, input.IsSame(&output), "fmt out not same")
*/
}
func TestMarshal(t *testing.T) {
input := Credentials{
UserID: "",
Name: "007",
Emails: "ja@pm.me;aj@cus.tom",
APIToken: "sdfdsfsdfsdfsdf",
MailboxPassword: "cdcdcdcd",
BridgePassword: "wew123",
Version: "k11",
Timestamp: 152469263742,
IsHidden: true,
IsCombinedAddressMode: false,
}
fmt.Printf("input %#v\n", input)
secret := input.Marshal()
fmt.Printf("secret %#v %d\n", secret, len(secret))
output := Credentials{APIToken: "refresh"}
require.NoError(t, output.Unmarshal(secret))
fmt.Printf("output %#v\n", output)
assert.Equal(t, input, output)
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2020 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/>.
// Code generated by ./credits.sh at Mon Apr 6 08:14:14 CEST 2020. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;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-imap-quota;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/danieljoos/wincred;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-quota;github.com/emersion/go-imap-specialuse;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/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;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;"

View File

@ -0,0 +1,107 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./listener/listener.go
// Package bridge is a generated GoMock package.
package bridge
import (
reflect "reflect"
time "time"
gomock "github.com/golang/mock/gomock"
)
// MockListener is a mock of Listener interface
type MockListener struct {
ctrl *gomock.Controller
recorder *MockListenerMockRecorder
}
// MockListenerMockRecorder is the mock recorder for MockListener
type MockListenerMockRecorder struct {
mock *MockListener
}
// NewMockListener creates a new mock instance
func NewMockListener(ctrl *gomock.Controller) *MockListener {
mock := &MockListener{ctrl: ctrl}
mock.recorder = &MockListenerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockListener) EXPECT() *MockListenerMockRecorder {
return m.recorder
}
// SetLimit mocks base method
func (m *MockListener) SetLimit(eventName string, limit time.Duration) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetLimit", eventName, limit)
}
// SetLimit indicates an expected call of SetLimit
func (mr *MockListenerMockRecorder) SetLimit(eventName, limit interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockListener)(nil).SetLimit), eventName, limit)
}
// Add mocks base method
func (m *MockListener) Add(eventName string, channel chan<- string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Add", eventName, channel)
}
// Add indicates an expected call of Add
func (mr *MockListenerMockRecorder) Add(eventName, channel interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockListener)(nil).Add), eventName, channel)
}
// Remove mocks base method
func (m *MockListener) Remove(eventName string, channel chan<- string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Remove", eventName, channel)
}
// Remove indicates an expected call of Remove
func (mr *MockListenerMockRecorder) Remove(eventName, channel interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockListener)(nil).Remove), eventName, channel)
}
// Emit mocks base method
func (m *MockListener) Emit(eventName, data string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Emit", eventName, data)
}
// Emit indicates an expected call of Emit
func (mr *MockListenerMockRecorder) Emit(eventName, data interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), eventName, data)
}
// SetBuffer mocks base method
func (m *MockListener) SetBuffer(eventName string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetBuffer", eventName)
}
// SetBuffer indicates an expected call of SetBuffer
func (mr *MockListenerMockRecorder) SetBuffer(eventName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBuffer", reflect.TypeOf((*MockListener)(nil).SetBuffer), eventName)
}
// RetryEmit mocks base method
func (m *MockListener) RetryEmit(eventName string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "RetryEmit", eventName)
}
// RetryEmit indicates an expected call of RetryEmit
func (mr *MockListenerMockRecorder) RetryEmit(eventName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryEmit", reflect.TypeOf((*MockListener)(nil).RetryEmit), eventName)
}

View File

@ -0,0 +1,923 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/bridge (interfaces: Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer)
// Package mocks is a generated GoMock package.
package mocks
import (
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
crypto "github.com/ProtonMail/gopenpgp/crypto"
gomock "github.com/golang/mock/gomock"
io "io"
reflect "reflect"
)
// MockConfiger is a mock of Configer interface
type MockConfiger struct {
ctrl *gomock.Controller
recorder *MockConfigerMockRecorder
}
// MockConfigerMockRecorder is the mock recorder for MockConfiger
type MockConfigerMockRecorder struct {
mock *MockConfiger
}
// NewMockConfiger creates a new mock instance
func NewMockConfiger(ctrl *gomock.Controller) *MockConfiger {
mock := &MockConfiger{ctrl: ctrl}
mock.recorder = &MockConfigerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockConfiger) EXPECT() *MockConfigerMockRecorder {
return m.recorder
}
// ClearData mocks base method
func (m *MockConfiger) ClearData() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearData")
ret0, _ := ret[0].(error)
return ret0
}
// ClearData indicates an expected call of ClearData
func (mr *MockConfigerMockRecorder) ClearData() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearData", reflect.TypeOf((*MockConfiger)(nil).ClearData))
}
// GetAPIConfig mocks base method
func (m *MockConfiger) GetAPIConfig() *pmapi.ClientConfig {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAPIConfig")
ret0, _ := ret[0].(*pmapi.ClientConfig)
return ret0
}
// GetAPIConfig indicates an expected call of GetAPIConfig
func (mr *MockConfigerMockRecorder) GetAPIConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIConfig", reflect.TypeOf((*MockConfiger)(nil).GetAPIConfig))
}
// GetDBDir mocks base method
func (m *MockConfiger) GetDBDir() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDBDir")
ret0, _ := ret[0].(string)
return ret0
}
// GetDBDir indicates an expected call of GetDBDir
func (mr *MockConfigerMockRecorder) GetDBDir() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBDir", reflect.TypeOf((*MockConfiger)(nil).GetDBDir))
}
// GetIMAPCachePath mocks base method
func (m *MockConfiger) GetIMAPCachePath() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIMAPCachePath")
ret0, _ := ret[0].(string)
return ret0
}
// GetIMAPCachePath indicates an expected call of GetIMAPCachePath
func (mr *MockConfigerMockRecorder) GetIMAPCachePath() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIMAPCachePath", reflect.TypeOf((*MockConfiger)(nil).GetIMAPCachePath))
}
// MockPreferenceProvider is a mock of PreferenceProvider interface
type MockPreferenceProvider struct {
ctrl *gomock.Controller
recorder *MockPreferenceProviderMockRecorder
}
// MockPreferenceProviderMockRecorder is the mock recorder for MockPreferenceProvider
type MockPreferenceProviderMockRecorder struct {
mock *MockPreferenceProvider
}
// NewMockPreferenceProvider creates a new mock instance
func NewMockPreferenceProvider(ctrl *gomock.Controller) *MockPreferenceProvider {
mock := &MockPreferenceProvider{ctrl: ctrl}
mock.recorder = &MockPreferenceProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPreferenceProvider) EXPECT() *MockPreferenceProviderMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockPreferenceProvider) Get(arg0 string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// Get indicates an expected call of Get
func (mr *MockPreferenceProviderMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPreferenceProvider)(nil).Get), arg0)
}
// GetBool mocks base method
func (m *MockPreferenceProvider) GetBool(arg0 string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBool", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// GetBool indicates an expected call of GetBool
func (mr *MockPreferenceProviderMockRecorder) GetBool(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockPreferenceProvider)(nil).GetBool), arg0)
}
// GetInt mocks base method
func (m *MockPreferenceProvider) GetInt(arg0 string) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt", arg0)
ret0, _ := ret[0].(int)
return ret0
}
// GetInt indicates an expected call of GetInt
func (mr *MockPreferenceProviderMockRecorder) GetInt(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockPreferenceProvider)(nil).GetInt), arg0)
}
// Set mocks base method
func (m *MockPreferenceProvider) Set(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", arg0, arg1)
}
// Set indicates an expected call of Set
func (mr *MockPreferenceProviderMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockPreferenceProvider)(nil).Set), arg0, arg1)
}
// MockPanicHandler is a mock of PanicHandler interface
type MockPanicHandler struct {
ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder
}
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler
type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler
}
// NewMockPanicHandler creates a new mock instance
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder
}
// HandlePanic mocks base method
func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic")
}
// HandlePanic indicates an expected call of HandlePanic
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
}
// MockPMAPIProvider is a mock of PMAPIProvider interface
type MockPMAPIProvider struct {
ctrl *gomock.Controller
recorder *MockPMAPIProviderMockRecorder
}
// MockPMAPIProviderMockRecorder is the mock recorder for MockPMAPIProvider
type MockPMAPIProviderMockRecorder struct {
mock *MockPMAPIProvider
}
// NewMockPMAPIProvider creates a new mock instance
func NewMockPMAPIProvider(ctrl *gomock.Controller) *MockPMAPIProvider {
mock := &MockPMAPIProvider{ctrl: ctrl}
mock.recorder = &MockPMAPIProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPMAPIProvider) EXPECT() *MockPMAPIProviderMockRecorder {
return m.recorder
}
// Addresses mocks base method
func (m *MockPMAPIProvider) Addresses() pmapi.AddressList {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Addresses")
ret0, _ := ret[0].(pmapi.AddressList)
return ret0
}
// Addresses indicates an expected call of Addresses
func (mr *MockPMAPIProviderMockRecorder) Addresses() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addresses", reflect.TypeOf((*MockPMAPIProvider)(nil).Addresses))
}
// Auth mocks base method
func (m *MockPMAPIProvider) Auth(arg0, arg1 string, arg2 *pmapi.AuthInfo) (*pmapi.Auth, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Auth", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Auth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Auth indicates an expected call of Auth
func (mr *MockPMAPIProviderMockRecorder) Auth(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth", reflect.TypeOf((*MockPMAPIProvider)(nil).Auth), arg0, arg1, arg2)
}
// Auth2FA mocks base method
func (m *MockPMAPIProvider) Auth2FA(arg0 string, arg1 *pmapi.Auth) (*pmapi.Auth2FA, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Auth2FA", arg0, arg1)
ret0, _ := ret[0].(*pmapi.Auth2FA)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Auth2FA indicates an expected call of Auth2FA
func (mr *MockPMAPIProviderMockRecorder) Auth2FA(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth2FA", reflect.TypeOf((*MockPMAPIProvider)(nil).Auth2FA), arg0, arg1)
}
// AuthInfo mocks base method
func (m *MockPMAPIProvider) AuthInfo(arg0 string) (*pmapi.AuthInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthInfo", arg0)
ret0, _ := ret[0].(*pmapi.AuthInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthInfo indicates an expected call of AuthInfo
func (mr *MockPMAPIProviderMockRecorder) AuthInfo(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthInfo", reflect.TypeOf((*MockPMAPIProvider)(nil).AuthInfo), arg0)
}
// AuthRefresh mocks base method
func (m *MockPMAPIProvider) AuthRefresh(arg0 string) (*pmapi.Auth, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthRefresh", arg0)
ret0, _ := ret[0].(*pmapi.Auth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthRefresh indicates an expected call of AuthRefresh
func (mr *MockPMAPIProviderMockRecorder) AuthRefresh(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthRefresh", reflect.TypeOf((*MockPMAPIProvider)(nil).AuthRefresh), arg0)
}
// CountMessages mocks base method
func (m *MockPMAPIProvider) CountMessages(arg0 string) ([]*pmapi.MessagesCount, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountMessages", arg0)
ret0, _ := ret[0].([]*pmapi.MessagesCount)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountMessages indicates an expected call of CountMessages
func (mr *MockPMAPIProviderMockRecorder) CountMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).CountMessages), arg0)
}
// CreateAttachment mocks base method
func (m *MockPMAPIProvider) CreateAttachment(arg0 *pmapi.Attachment, arg1, arg2 io.Reader) (*pmapi.Attachment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateAttachment", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Attachment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateAttachment indicates an expected call of CreateAttachment
func (mr *MockPMAPIProviderMockRecorder) CreateAttachment(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAttachment", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateAttachment), arg0, arg1, arg2)
}
// CreateDraft mocks base method
func (m *MockPMAPIProvider) CreateDraft(arg0 *pmapi.Message, arg1 string, arg2 int) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDraft", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDraft indicates an expected call of CreateDraft
func (mr *MockPMAPIProviderMockRecorder) CreateDraft(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDraft", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateDraft), arg0, arg1, arg2)
}
// CreateLabel mocks base method
func (m *MockPMAPIProvider) CreateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateLabel", arg0)
ret0, _ := ret[0].(*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateLabel indicates an expected call of CreateLabel
func (mr *MockPMAPIProviderMockRecorder) CreateLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateLabel), arg0)
}
// CurrentUser mocks base method
func (m *MockPMAPIProvider) CurrentUser() (*pmapi.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CurrentUser")
ret0, _ := ret[0].(*pmapi.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CurrentUser indicates an expected call of CurrentUser
func (mr *MockPMAPIProviderMockRecorder) CurrentUser() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentUser", reflect.TypeOf((*MockPMAPIProvider)(nil).CurrentUser))
}
// DecryptAndVerifyCards mocks base method
func (m *MockPMAPIProvider) DecryptAndVerifyCards(arg0 []pmapi.Card) ([]pmapi.Card, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DecryptAndVerifyCards", arg0)
ret0, _ := ret[0].([]pmapi.Card)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DecryptAndVerifyCards indicates an expected call of DecryptAndVerifyCards
func (mr *MockPMAPIProviderMockRecorder) DecryptAndVerifyCards(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptAndVerifyCards", reflect.TypeOf((*MockPMAPIProvider)(nil).DecryptAndVerifyCards), arg0)
}
// DeleteLabel mocks base method
func (m *MockPMAPIProvider) DeleteLabel(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteLabel", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteLabel indicates an expected call of DeleteLabel
func (mr *MockPMAPIProviderMockRecorder) DeleteLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).DeleteLabel), arg0)
}
// DeleteMessages mocks base method
func (m *MockPMAPIProvider) DeleteMessages(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteMessages", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteMessages indicates an expected call of DeleteMessages
func (mr *MockPMAPIProviderMockRecorder) DeleteMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).DeleteMessages), arg0)
}
// EmptyFolder mocks base method
func (m *MockPMAPIProvider) EmptyFolder(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EmptyFolder", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// EmptyFolder indicates an expected call of EmptyFolder
func (mr *MockPMAPIProviderMockRecorder) EmptyFolder(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EmptyFolder", reflect.TypeOf((*MockPMAPIProvider)(nil).EmptyFolder), arg0, arg1)
}
// GetAttachment mocks base method
func (m *MockPMAPIProvider) GetAttachment(arg0 string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAttachment", arg0)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAttachment indicates an expected call of GetAttachment
func (mr *MockPMAPIProviderMockRecorder) GetAttachment(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockPMAPIProvider)(nil).GetAttachment), arg0)
}
// GetContactByID mocks base method
func (m *MockPMAPIProvider) GetContactByID(arg0 string) (pmapi.Contact, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContactByID", arg0)
ret0, _ := ret[0].(pmapi.Contact)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetContactByID indicates an expected call of GetContactByID
func (mr *MockPMAPIProviderMockRecorder) GetContactByID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactByID", reflect.TypeOf((*MockPMAPIProvider)(nil).GetContactByID), arg0)
}
// GetContactEmailByEmail mocks base method
func (m *MockPMAPIProvider) GetContactEmailByEmail(arg0 string, arg1, arg2 int) ([]pmapi.ContactEmail, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContactEmailByEmail", arg0, arg1, arg2)
ret0, _ := ret[0].([]pmapi.ContactEmail)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetContactEmailByEmail indicates an expected call of GetContactEmailByEmail
func (mr *MockPMAPIProviderMockRecorder) GetContactEmailByEmail(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactEmailByEmail", reflect.TypeOf((*MockPMAPIProvider)(nil).GetContactEmailByEmail), arg0, arg1, arg2)
}
// GetEvent mocks base method
func (m *MockPMAPIProvider) GetEvent(arg0 string) (*pmapi.Event, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEvent", arg0)
ret0, _ := ret[0].(*pmapi.Event)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetEvent indicates an expected call of GetEvent
func (mr *MockPMAPIProviderMockRecorder) GetEvent(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvent", reflect.TypeOf((*MockPMAPIProvider)(nil).GetEvent), arg0)
}
// GetMailSettings mocks base method
func (m *MockPMAPIProvider) GetMailSettings() (pmapi.MailSettings, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMailSettings")
ret0, _ := ret[0].(pmapi.MailSettings)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMailSettings indicates an expected call of GetMailSettings
func (mr *MockPMAPIProviderMockRecorder) GetMailSettings() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailSettings", reflect.TypeOf((*MockPMAPIProvider)(nil).GetMailSettings))
}
// GetMessage mocks base method
func (m *MockPMAPIProvider) GetMessage(arg0 string) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMessage", arg0)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMessage indicates an expected call of GetMessage
func (mr *MockPMAPIProviderMockRecorder) GetMessage(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockPMAPIProvider)(nil).GetMessage), arg0)
}
// GetPublicKeysForEmail mocks base method
func (m *MockPMAPIProvider) GetPublicKeysForEmail(arg0 string) ([]pmapi.PublicKey, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPublicKeysForEmail", arg0)
ret0, _ := ret[0].([]pmapi.PublicKey)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetPublicKeysForEmail indicates an expected call of GetPublicKeysForEmail
func (mr *MockPMAPIProviderMockRecorder) GetPublicKeysForEmail(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeysForEmail", reflect.TypeOf((*MockPMAPIProvider)(nil).GetPublicKeysForEmail), arg0)
}
// Import mocks base method
func (m *MockPMAPIProvider) Import(arg0 []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Import", arg0)
ret0, _ := ret[0].([]*pmapi.ImportMsgRes)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Import indicates an expected call of Import
func (mr *MockPMAPIProviderMockRecorder) Import(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*MockPMAPIProvider)(nil).Import), arg0)
}
// KeyRingForAddressID mocks base method
func (m *MockPMAPIProvider) KeyRingForAddressID(arg0 string) *crypto.KeyRing {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
return ret0
}
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID
func (mr *MockPMAPIProviderMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockPMAPIProvider)(nil).KeyRingForAddressID), arg0)
}
// LabelMessages mocks base method
func (m *MockPMAPIProvider) LabelMessages(arg0 []string, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LabelMessages", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// LabelMessages indicates an expected call of LabelMessages
func (mr *MockPMAPIProviderMockRecorder) LabelMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).LabelMessages), arg0, arg1)
}
// ListLabels mocks base method
func (m *MockPMAPIProvider) ListLabels() ([]*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListLabels")
ret0, _ := ret[0].([]*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListLabels indicates an expected call of ListLabels
func (mr *MockPMAPIProviderMockRecorder) ListLabels() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLabels", reflect.TypeOf((*MockPMAPIProvider)(nil).ListLabels))
}
// ListMessages mocks base method
func (m *MockPMAPIProvider) ListMessages(arg0 *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListMessages", arg0)
ret0, _ := ret[0].([]*pmapi.Message)
ret1, _ := ret[1].(int)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListMessages indicates an expected call of ListMessages
func (mr *MockPMAPIProviderMockRecorder) ListMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).ListMessages), arg0)
}
// Logout mocks base method
func (m *MockPMAPIProvider) Logout() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout")
ret0, _ := ret[0].(error)
return ret0
}
// Logout indicates an expected call of Logout
func (mr *MockPMAPIProviderMockRecorder) Logout() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockPMAPIProvider)(nil).Logout))
}
// MarkMessagesRead mocks base method
func (m *MockPMAPIProvider) MarkMessagesRead(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarkMessagesRead", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// MarkMessagesRead indicates an expected call of MarkMessagesRead
func (mr *MockPMAPIProviderMockRecorder) MarkMessagesRead(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesRead", reflect.TypeOf((*MockPMAPIProvider)(nil).MarkMessagesRead), arg0)
}
// MarkMessagesUnread mocks base method
func (m *MockPMAPIProvider) MarkMessagesUnread(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarkMessagesUnread", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// MarkMessagesUnread indicates an expected call of MarkMessagesUnread
func (mr *MockPMAPIProviderMockRecorder) MarkMessagesUnread(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesUnread", reflect.TypeOf((*MockPMAPIProvider)(nil).MarkMessagesUnread), arg0)
}
// ReportBugWithEmailClient mocks base method
func (m *MockPMAPIProvider) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportBugWithEmailClient", arg0, arg1, arg2, arg3, arg4, arg5, arg6)
ret0, _ := ret[0].(error)
return ret0
}
// ReportBugWithEmailClient indicates an expected call of ReportBugWithEmailClient
func (mr *MockPMAPIProviderMockRecorder) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBugWithEmailClient", reflect.TypeOf((*MockPMAPIProvider)(nil).ReportBugWithEmailClient), arg0, arg1, arg2, arg3, arg4, arg5, arg6)
}
// SendMessage mocks base method
func (m *MockPMAPIProvider) SendMessage(arg0 string, arg1 *pmapi.SendMessageReq) (*pmapi.Message, *pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendMessage", arg0, arg1)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(*pmapi.Message)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// SendMessage indicates an expected call of SendMessage
func (mr *MockPMAPIProviderMockRecorder) SendMessage(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockPMAPIProvider)(nil).SendMessage), arg0, arg1)
}
// SendSimpleMetric mocks base method
func (m *MockPMAPIProvider) SendSimpleMetric(arg0, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendSimpleMetric", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// SendSimpleMetric indicates an expected call of SendSimpleMetric
func (mr *MockPMAPIProviderMockRecorder) SendSimpleMetric(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendSimpleMetric", reflect.TypeOf((*MockPMAPIProvider)(nil).SendSimpleMetric), arg0, arg1, arg2)
}
// SetAuths mocks base method
func (m *MockPMAPIProvider) SetAuths(arg0 chan<- *pmapi.Auth) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetAuths", arg0)
}
// SetAuths indicates an expected call of SetAuths
func (mr *MockPMAPIProviderMockRecorder) SetAuths(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAuths", reflect.TypeOf((*MockPMAPIProvider)(nil).SetAuths), arg0)
}
// UnlabelMessages mocks base method
func (m *MockPMAPIProvider) UnlabelMessages(arg0 []string, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnlabelMessages", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UnlabelMessages indicates an expected call of UnlabelMessages
func (mr *MockPMAPIProviderMockRecorder) UnlabelMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlabelMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).UnlabelMessages), arg0, arg1)
}
// Unlock mocks base method
func (m *MockPMAPIProvider) Unlock(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unlock", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Unlock indicates an expected call of Unlock
func (mr *MockPMAPIProviderMockRecorder) Unlock(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockPMAPIProvider)(nil).Unlock), arg0)
}
// UnlockAddresses mocks base method
func (m *MockPMAPIProvider) UnlockAddresses(arg0 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnlockAddresses", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UnlockAddresses indicates an expected call of UnlockAddresses
func (mr *MockPMAPIProviderMockRecorder) UnlockAddresses(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlockAddresses", reflect.TypeOf((*MockPMAPIProvider)(nil).UnlockAddresses), arg0)
}
// UpdateLabel mocks base method
func (m *MockPMAPIProvider) UpdateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateLabel", arg0)
ret0, _ := ret[0].(*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateLabel indicates an expected call of UpdateLabel
func (mr *MockPMAPIProviderMockRecorder) UpdateLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).UpdateLabel), arg0)
}
// UpdateUser mocks base method
func (m *MockPMAPIProvider) UpdateUser() (*pmapi.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser")
ret0, _ := ret[0].(*pmapi.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser
func (mr *MockPMAPIProviderMockRecorder) UpdateUser() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockPMAPIProvider)(nil).UpdateUser))
}
// MockCredentialsStorer is a mock of CredentialsStorer interface
type MockCredentialsStorer struct {
ctrl *gomock.Controller
recorder *MockCredentialsStorerMockRecorder
}
// MockCredentialsStorerMockRecorder is the mock recorder for MockCredentialsStorer
type MockCredentialsStorerMockRecorder struct {
mock *MockCredentialsStorer
}
// NewMockCredentialsStorer creates a new mock instance
func NewMockCredentialsStorer(ctrl *gomock.Controller) *MockCredentialsStorer {
mock := &MockCredentialsStorer{ctrl: ctrl}
mock.recorder = &MockCredentialsStorerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockCredentialsStorer) EXPECT() *MockCredentialsStorerMockRecorder {
return m.recorder
}
// Add mocks base method
func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3 string, arg4 []string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Add", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(*credentials.Credentials)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Add indicates an expected call of Add
func (mr *MockCredentialsStorerMockRecorder) Add(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockCredentialsStorer)(nil).Add), arg0, arg1, arg2, arg3, arg4)
}
// Delete mocks base method
func (m *MockCredentialsStorer) Delete(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockCredentialsStorerMockRecorder) Delete(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCredentialsStorer)(nil).Delete), arg0)
}
// Get mocks base method
func (m *MockCredentialsStorer) Get(arg0 string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(*credentials.Credentials)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockCredentialsStorerMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCredentialsStorer)(nil).Get), arg0)
}
// List mocks base method
func (m *MockCredentialsStorer) List() ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockCredentialsStorerMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCredentialsStorer)(nil).List))
}
// Logout mocks base method
func (m *MockCredentialsStorer) Logout(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Logout indicates an expected call of Logout
func (mr *MockCredentialsStorerMockRecorder) Logout(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockCredentialsStorer)(nil).Logout), arg0)
}
// SwitchAddressMode mocks base method
func (m *MockCredentialsStorer) SwitchAddressMode(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwitchAddressMode", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SwitchAddressMode indicates an expected call of SwitchAddressMode
func (mr *MockCredentialsStorerMockRecorder) SwitchAddressMode(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwitchAddressMode", reflect.TypeOf((*MockCredentialsStorer)(nil).SwitchAddressMode), arg0)
}
// UpdateEmails mocks base method
func (m *MockCredentialsStorer) UpdateEmails(arg0 string, arg1 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateEmails", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateEmails indicates an expected call of UpdateEmails
func (mr *MockCredentialsStorerMockRecorder) UpdateEmails(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmails", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateEmails), arg0, arg1)
}
// UpdateToken mocks base method
func (m *MockCredentialsStorer) UpdateToken(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateToken", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateToken indicates an expected call of UpdateToken
func (mr *MockCredentialsStorerMockRecorder) UpdateToken(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateToken", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateToken), arg0, arg1)
}

View File

@ -0,0 +1,34 @@
// Copyright (c) 2020 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/>.
// Code generated by ./release-notes.sh at Mon Apr 6 08:14:14 CEST 2020. DO NOT EDIT.
package bridge
const ReleaseNotes = `NOTE: We recommend to reconfigure your email client after upgrading to ensure the best results with the new draft folder support
• Faster and more resilient mail synchronization process, especially for large mailboxes
• Added "Alternate Routing" feature to mitigate blocking of Proton Servers
• Added synchronization of draft folder
• Improved event handling when there are frequent changes
• Security improvements for loading dependent libraries
• Minor UI & API communication tweaks
`
const ReleaseFixedBugs = `• Fixed rare case of sending the same message multiple times in Outlook
• Fixed bug in macOS update process; available from next update
`

105
internal/bridge/types.go Normal file
View File

@ -0,0 +1,105 @@
// Copyright (c) 2020 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
import (
"io"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" // mockgen needs this to be given an explicit import name
)
type Configer interface {
ClearData() error
GetDBDir() string
GetIMAPCachePath() string
GetAPIConfig() *pmapi.ClientConfig
}
type PreferenceProvider interface {
Get(key string) string
GetBool(key string) bool
GetInt(key string) int
Set(key string, value string)
}
type PanicHandler interface {
HandlePanic()
}
type PMAPIProviderFactory func(string) PMAPIProvider
type PMAPIProvider interface {
SetAuths(auths chan<- *pmapi.Auth)
Auth(username, password string, info *pmapi.AuthInfo) (*pmapi.Auth, error)
AuthInfo(username string) (*pmapi.AuthInfo, error)
AuthRefresh(token string) (*pmapi.Auth, error)
Unlock(mailboxPassword string) (kr *pmcrypto.KeyRing, err error)
UnlockAddresses(passphrase []byte) error
CurrentUser() (*pmapi.User, error)
UpdateUser() (*pmapi.User, error)
Addresses() pmapi.AddressList
Logout() error
GetEvent(eventID string) (*pmapi.Event, error)
CountMessages(addressID string) ([]*pmapi.MessagesCount, error)
ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Message, int, error)
GetMessage(apiID string) (*pmapi.Message, error)
Import([]*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error)
DeleteMessages(apiIDs []string) error
LabelMessages(apiIDs []string, labelID string) error
UnlabelMessages(apiIDs []string, labelID string) error
MarkMessagesRead(apiIDs []string) error
MarkMessagesUnread(apiIDs []string) error
ListLabels() ([]*pmapi.Label, error)
CreateLabel(label *pmapi.Label) (*pmapi.Label, error)
UpdateLabel(label *pmapi.Label) (*pmapi.Label, error)
DeleteLabel(labelID string) error
EmptyFolder(labelID string, addressID string) error
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
SendSimpleMetric(category, action, label string) error
Auth2FA(twoFactorCode string, auth *pmapi.Auth) (*pmapi.Auth2FA, error)
GetMailSettings() (pmapi.MailSettings, error)
GetContactEmailByEmail(string, int, int) ([]pmapi.ContactEmail, error)
GetContactByID(string) (pmapi.Contact, error)
DecryptAndVerifyCards([]pmapi.Card) ([]pmapi.Card, error)
GetPublicKeysForEmail(string) ([]pmapi.PublicKey, bool, error)
SendMessage(string, *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err error)
CreateDraft(m *pmapi.Message, parent string, action int) (created *pmapi.Message, err error)
CreateAttachment(att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error)
KeyRingForAddressID(string) (kr *pmcrypto.KeyRing)
GetAttachment(id string) (att io.ReadCloser, err error)
}
type CredentialsStorer interface {
List() (userIDs []string, err error)
Add(userID, userName, apiToken, mailboxPassword string, emails []string) (*credentials.Credentials, error)
Get(userID string) (*credentials.Credentials, error)
SwitchAddressMode(userID string) error
UpdateEmails(userID string, emails []string) error
UpdateToken(userID, apiToken string) error
Logout(userID string) error
Delete(userID string) error
}

621
internal/bridge/user.go Normal file
View File

@ -0,0 +1,621 @@
// Copyright (c) 2020 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
import (
"fmt"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
logrus "github.com/sirupsen/logrus"
)
// ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from bridge.
var ErrLoggedOutUser = errors.New("bridge account is logged out, use bridge to login again")
// User is a struct on top of API client and credentials store.
type User struct {
log *logrus.Entry
panicHandler PanicHandler
listener listener.Listener
apiClient PMAPIProvider
credStorer CredentialsStorer
imapUpdatesChannel chan interface{}
store *store.Store
storeCache *store.Cache
storePath string
userID string
creds *credentials.Credentials
lock sync.RWMutex
authChannel chan *pmapi.Auth
hasAPIAuth bool
unlockingKeyringLock sync.Mutex
wasKeyringUnlocked bool
}
// newUser creates a new bridge user.
func newUser(
panicHandler PanicHandler,
userID string,
eventListener listener.Listener,
credStorer CredentialsStorer,
apiClient PMAPIProvider,
storeCache *store.Cache,
storeDir string,
) (u *User, err error) {
log := log.WithField("user", userID)
log.Debug("Creating or loading user")
creds, err := credStorer.Get(userID)
if err != nil {
return nil, errors.Wrap(err, "failed to load user credentials")
}
u = &User{
log: log,
panicHandler: panicHandler,
listener: eventListener,
credStorer: credStorer,
apiClient: apiClient,
storeCache: storeCache,
storePath: getUserStorePath(storeDir, userID),
userID: userID,
creds: creds,
}
return
}
// init initialises a bridge user. This includes reloading its credentials from the credentials store
// (such as when logging out and back in, you need to reload the credentials because the new credentials will
// have the apitoken and password), authorising the user against the api, loading the user store (creating a new one
// if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if
// something in the store changed).
func (u *User) init(idleUpdates chan interface{}, apiClient PMAPIProvider) (err error) {
// If this is an existing user, we still need a new api client to get a new refresh token.
// If it's a new user, doesn't matter really; this is basically a noop in this case.
u.apiClient = apiClient
u.unlockingKeyringLock.Lock()
u.wasKeyringUnlocked = false
u.unlockingKeyringLock.Unlock()
// Reload the user's credentials (if they log out and back in we need the new
// version with the apitoken and mailbox password).
creds, err := u.credStorer.Get(u.userID)
if err != nil {
return errors.Wrap(err, "failed to load user credentials")
}
u.creds = creds
// Set up the auth channel on which auths from the api client are sent.
u.authChannel = make(chan *pmapi.Auth)
u.apiClient.SetAuths(u.authChannel)
u.hasAPIAuth = false
go func() {
defer u.panicHandler.HandlePanic()
u.watchAPIClientAuths()
}()
// Try to authorise the user if they aren't already authorised.
// Note: we still allow users to set up bridge if the internet is off.
if authErr := u.authorizeIfNecessary(false); authErr != nil {
switch errors.Cause(authErr) {
case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser:
u.log.WithError(authErr).Warn("Could not authorize user")
default:
if logoutErr := u.logout(); logoutErr != nil {
u.log.WithError(logoutErr).Warn("Could not logout user")
}
return errors.Wrap(authErr, "failed to authorize user")
}
}
// Logged-out user keeps store running to access offline data.
// Therefore it is necessary to close it before re-init.
if u.store != nil {
if err := u.store.Close(); err != nil {
log.WithError(err).Error("Not able to close store")
}
u.store = nil
}
store, err := store.New(u.panicHandler, u, u.apiClient, u.listener, u.storePath, u.storeCache)
if err != nil {
return errors.Wrap(err, "failed to create store")
}
u.store = store
// Save the imap updates channel here so it can be set later when imap connects.
u.imapUpdatesChannel = idleUpdates
return err
}
func (u *User) SetIMAPIdleUpdateChannel() {
if u.store == nil {
return
}
u.store.SetIMAPUpdateChannel(u.imapUpdatesChannel)
}
// authorizeIfNecessary checks whether user is logged in and is connected to api auth channel.
// If user is not already connected to the api auth channel (for example there was no internet during start),
// it tries to connect it. See `connectToAuthChannel` for more info.
func (u *User) authorizeIfNecessary(emitEvent bool) (err error) {
// If user is connected and has an auth channel, then perfect, nothing to do here.
if u.creds.IsConnected() && u.HasAPIAuth() {
// The keyring unlock is triggered here to resolve state where apiClient
// is authenticated (we have auth token) but it was not possible to download
// and unlock the keys (internet not reachable).
return u.unlockIfNecessary()
}
if !u.creds.IsConnected() {
err = ErrLoggedOutUser
} else if err = u.authorizeAndUnlock(); err != nil {
u.log.WithError(err).Error("Could not authorize and unlock user")
switch errors.Cause(err) {
case pmapi.ErrUpgradeApplication:
u.listener.Emit(events.UpgradeApplicationEvent, "")
case pmapi.ErrAPINotReachable:
u.listener.Emit(events.InternetOffEvent, "")
default:
if errLogout := u.credStorer.Logout(u.userID); errLogout != nil {
u.log.WithField("err", errLogout).Error("Could not log user out from credentials store")
}
}
}
if emitEvent && err != nil &&
errors.Cause(err) != pmapi.ErrUpgradeApplication &&
errors.Cause(err) != pmapi.ErrAPINotReachable {
u.listener.Emit(events.LogoutEvent, u.userID)
}
return err
}
// unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked.
func (u *User) unlockIfNecessary() error {
u.unlockingKeyringLock.Lock()
defer u.unlockingKeyringLock.Unlock()
if u.wasKeyringUnlocked {
return nil
}
if _, err := u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
return errors.Wrap(err, "failed to unlock user")
}
if err := u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return errors.Wrap(err, "failed to unlock user addresses")
}
u.wasKeyringUnlocked = true
return nil
}
// authorizeAndUnlock tries to authorize the user with the API using the the user's APIToken.
// If that succeeds, it tries to unlock the user's keys and addresses.
func (u *User) authorizeAndUnlock() (err error) {
if u.creds.APIToken == "" {
u.log.Warn("Could not connect to API auth channel, have no API token")
return nil
}
auth, err := u.apiClient.AuthRefresh(u.creds.APIToken)
if err != nil {
return errors.Wrap(err, "failed to refresh API auth")
}
u.authChannel <- auth
if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
return errors.Wrap(err, "failed to unlock user")
}
if err = u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return errors.Wrap(err, "failed to unlock user addresses")
}
return nil
}
// See `connectToAPIClientAuthChannel` for more info.
func (u *User) watchAPIClientAuths() {
for auth := range u.authChannel {
if auth != nil {
newRefreshToken := auth.UID() + ":" + auth.RefreshToken
u.updateAPIToken(newRefreshToken)
u.hasAPIAuth = true
} else if err := u.logout(); err != nil {
u.log.WithError(err).Error("Cannot logout user after receiving empty auth from API")
}
}
}
// updateAPIToken is helper for updating the token in keychain. It's not supposed to be
// called directly from other parts of the code--only from `watchAPIClientAuths`.
func (u *User) updateAPIToken(newRefreshToken string) {
u.lock.Lock()
defer u.lock.Unlock()
u.log.Info("Saving refresh token")
if err := u.credStorer.UpdateToken(u.userID, newRefreshToken); err != nil {
u.log.WithError(err).Error("Cannot update refresh token in credentials store")
} else {
u.refreshFromCredentials()
}
}
// clearStore removes the database.
func (u *User) clearStore() error {
u.log.Trace("Clearing user store")
if u.store != nil {
if err := u.store.Remove(); err != nil {
return errors.Wrap(err, "failed to remove store")
}
} else {
u.log.Warn("Store is not initialized: cleaning up store files manually")
if err := store.RemoveStore(u.storeCache, u.storePath, u.userID); err != nil {
return errors.Wrap(err, "failed to remove store manually")
}
}
return nil
}
// closeStore just closes the store without deleting it.
func (u *User) closeStore() error {
u.log.Trace("Closing user store")
if u.store != nil {
if err := u.store.Close(); err != nil {
return errors.Wrap(err, "failed to close store")
}
}
return nil
}
// getUserStorePath returns the file path of the store database for the given userID.
func getUserStorePath(storeDir string, userID string) (path string) {
fileName := fmt.Sprintf("mailbox-%v.db", userID)
return filepath.Join(storeDir, fileName)
}
// GetTemporaryPMAPIClient returns an authorised PMAPI client.
// Do not use! It's only for backward compatibility of old SMTP and IMAP implementations.
// After proper refactor of SMTP and IMAP remove this method.
func (u *User) GetTemporaryPMAPIClient() PMAPIProvider {
return u.apiClient
}
// ID returns the user's userID.
func (u *User) ID() string {
return u.userID
}
// Username returns the user's username as found in the user's credentials.
func (u *User) Username() string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.Name
}
// IsConnected returns whether user is logged in.
func (u *User) IsConnected() bool {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.IsConnected()
}
// IsCombinedAddressMode returns whether user is set in combined or split mode.
// Combined mode is the default mode and is what users typically need.
// Split mode is mostly for outlook as it cannot handle sending e-mails from an
// address other than the primary one.
func (u *User) IsCombinedAddressMode() bool {
if u.store != nil {
return u.store.IsCombinedMode()
}
return u.creds.IsCombinedAddressMode
}
// GetPrimaryAddress returns the user's original address (which is
// not necessarily the same as the primary address, because a primary address
// might be an alias and be in position one).
func (u *User) GetPrimaryAddress() string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.EmailList()[0]
}
// GetStoreAddresses returns all addresses used by the store (so in combined mode,
// that's just the original address, but in split mode, that's all active addresses).
func (u *User) GetStoreAddresses() []string {
u.lock.RLock()
defer u.lock.RUnlock()
if u.IsCombinedAddressMode() {
return u.creds.EmailList()[:1]
}
return u.creds.EmailList()
}
// getStoreAddresses returns a user's used addresses (with the original address in first place).
func (u *User) getStoreAddresses() []string { // nolint[unused]
addrInfo, err := u.store.GetAddressInfo()
if err != nil {
u.log.WithError(err).Error("Failed getting address info from store")
return nil
}
addresses := []string{}
for _, addr := range addrInfo {
addresses = append(addresses, addr.Address)
}
if u.IsCombinedAddressMode() {
return addresses[:1]
}
return addresses
}
// GetAddresses returns list of all addresses.
func (u *User) GetAddresses() []string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.EmailList()
}
// GetAddressID returns the API ID of the given address.
func (u *User) GetAddressID(address string) (id string, err error) {
u.lock.RLock()
defer u.lock.RUnlock()
address = strings.ToLower(address)
if u.store == nil {
err = errors.New("store is not initialised")
return
}
return u.store.GetAddressID(address)
}
// GetBridgePassword returns bridge password. This is not a password of the PM
// account, but generated password for local purposes to not use a PM account
// in the clients (such as Thunderbird).
func (u *User) GetBridgePassword() string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.BridgePassword
}
// CheckBridgeLogin checks whether the user is logged in and the bridge
// password is correct.
func (u *User) CheckBridgeLogin(password string) error {
if isApplicationOutdated {
u.listener.Emit(events.UpgradeApplicationEvent, "")
return pmapi.ErrUpgradeApplication
}
u.lock.RLock()
defer u.lock.RUnlock()
// True here because users should be notified by popup of auth failure.
if err := u.authorizeIfNecessary(true); err != nil {
u.log.WithError(err).Error("Failed to authorize user")
return err
}
return u.creds.CheckPassword(password)
}
// UpdateUser updates user details from API and saves to the credentials.
func (u *User) UpdateUser() error {
u.lock.Lock()
defer u.lock.Unlock()
if err := u.authorizeIfNecessary(true); err != nil {
return errors.Wrap(err, "cannot update user")
}
_, err := u.apiClient.UpdateUser()
if err != nil {
return err
}
if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
return err
}
if err := u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return err
}
emails := u.apiClient.Addresses().ActiveEmails()
if err := u.credStorer.UpdateEmails(u.userID, emails); err != nil {
return err
}
u.refreshFromCredentials()
return nil
}
// SwitchAddressMode changes mode from combined to split and vice versa. The mode to switch to is determined by the
// state of the user's credentials in the credentials store. See `IsCombinedAddressMode` for more details.
func (u *User) SwitchAddressMode() (err error) {
u.log.Trace("Switching user address mode")
u.lock.Lock()
defer u.lock.Unlock()
u.closeAllConnections()
if u.store == nil {
err = errors.New("store is not initialised")
return
}
newAddressModeState := !u.IsCombinedAddressMode()
if err = u.store.UseCombinedMode(newAddressModeState); err != nil {
u.log.WithError(err).Error("Could not switch store address mode")
return
}
if u.creds.IsCombinedAddressMode != newAddressModeState {
if err = u.credStorer.SwitchAddressMode(u.userID); err != nil {
u.log.WithError(err).Error("Could not switch credentials store address mode")
return
}
}
u.refreshFromCredentials()
return err
}
// logout is the same as Logout, but for internal purposes (logged out from
// the server) which emits LogoutEvent to notify other parts of the Bridge.
func (u *User) logout() error {
u.lock.Lock()
wasConnected := u.creds.IsConnected()
u.lock.Unlock()
err := u.Logout()
if wasConnected {
u.listener.Emit(events.LogoutEvent, u.userID)
u.listener.Emit(events.UserRefreshEvent, u.userID)
}
return err
}
// Logout logs out the user from pmapi, the credentials store, the mail store, and tries to remove as much
// sensitive data as possible.
func (u *User) Logout() (err error) {
u.lock.Lock()
defer u.lock.Unlock()
u.log.Debug("Logging out user")
if !u.creds.IsConnected() {
return
}
u.unlockingKeyringLock.Lock()
u.wasKeyringUnlocked = false
u.unlockingKeyringLock.Unlock()
if err = u.apiClient.Logout(); err != nil {
u.log.WithError(err).Warn("Could not log user out from API client")
}
u.apiClient.SetAuths(nil)
// Logout needs to stop auth channel so when user logs back in
// it can register again with new client.
// Note: be careful to not close channel twice.
if u.authChannel != nil {
close(u.authChannel)
u.authChannel = nil
}
if err = u.credStorer.Logout(u.userID); err != nil {
u.log.WithError(err).Warn("Could not log user out from credentials store")
if err = u.credStorer.Delete(u.userID); err != nil {
u.log.WithError(err).Error("Could not delete user from credentials store")
}
}
u.refreshFromCredentials()
// Do not close whole store, just event loop. Some information might be needed offline (e.g. addressID)
u.closeEventLoop()
u.closeAllConnections()
runtime.GC()
return err
}
func (u *User) refreshFromCredentials() {
if credentials, err := u.credStorer.Get(u.userID); err != nil {
log.Error("Cannot update credentials: ", err)
} else {
u.creds = credentials
}
}
func (u *User) closeEventLoop() {
if u.store == nil {
return
}
u.store.CloseEventLoop()
}
// closeAllConnections calls CloseConnection for all users addresses.
func (u *User) closeAllConnections() {
for _, address := range u.creds.EmailList() {
u.CloseConnection(address)
}
if u.store != nil {
u.store.SetIMAPUpdateChannel(nil)
}
}
// CloseConnection emits closeConnection event on `address` which should close all active connection.
func (u *User) CloseConnection(address string) {
u.listener.Emit(events.CloseConnectionEvent, address)
}
func (u *User) GetStore() *store.Store {
return u.store
}
func (u *User) HasAPIAuth() bool {
return u.hasAPIAuth
}

View File

@ -0,0 +1,209 @@
// Copyright (c) 2020 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
import (
"testing"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestUpdateUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().UpdateUser().Return(nil, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email})
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
assert.NoError(t, user.UpdateUser())
waitForEvents()
}
func TestUserSwitchAddressMode(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
assert.True(t, user.store.IsCombinedMode())
assert.True(t, user.creds.IsCombinedAddressMode)
assert.True(t, user.IsCombinedAddressMode())
waitForEvents()
gomock.InOrder(
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsSplit, nil),
)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
assert.NoError(t, user.SwitchAddressMode())
assert.False(t, user.store.IsCombinedMode())
assert.False(t, user.creds.IsCombinedAddressMode)
assert.False(t, user.IsCombinedAddressMode())
gomock.InOrder(
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me"),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me"),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "alsouser@pm.me"),
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
assert.NoError(t, user.SwitchAddressMode())
assert.True(t, user.store.IsCombinedMode())
assert.True(t, user.creds.IsCombinedAddressMode)
assert.True(t, user.IsCombinedAddressMode())
waitForEvents()
}
func TestLogoutUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := user.Logout()
waitForEvents()
assert.NoError(t, err)
}
func TestLogoutUserFailsLogout(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed"))
m.credentialsStore.EXPECT().Delete("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := user.Logout()
waitForEvents()
assert.NoError(t, err)
}
func TestCheckBridgeLogin(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
err := user.CheckBridgeLogin(testCredentials.BridgePassword)
waitForEvents()
assert.NoError(t, err)
}
func TestCheckBridgeLoginUpgradeApplication(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
isApplicationOutdated = true
err := user.CheckBridgeLogin("any-pass")
waitForEvents()
isApplicationOutdated = false
assert.Equal(t, pmapi.ErrUpgradeApplication, err)
}
func TestCheckBridgeLoginLoggedOut(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
m.pmapiClient.EXPECT().Addresses().Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
_ = user.init(nil, m.pmapiClient)
defer cleanUpUserData(user)
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
err := user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword)
waitForEvents()
assert.Equal(t, "bridge account is logged out, use bridge to login again", err.Error())
}
func TestCheckBridgeLoginBadPassword(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
err := user.CheckBridgeLogin("wrong!")
waitForEvents()
assert.Equal(t, "backend/credentials: incorrect password", err.Error())
}

View File

@ -0,0 +1,188 @@
// Copyright (c) 2020 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
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
a "github.com/stretchr/testify/assert"
)
func TestNewUserNoCredentialsStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("fail"))
_, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
a.Error(t, err)
}
func TestNewUserBridgeOutdated(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil).AnyTimes()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrUpgradeApplication).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "").AnyTimes()
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrUpgradeApplication)
m.pmapiClient.EXPECT().Addresses().Return(nil)
checkNewUser(m)
}
func TestNewUserNoInternetConnection(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.InternetOffEvent, "").AnyTimes()
m.pmapiClient.EXPECT().Addresses().Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrAPINotReachable)
m.pmapiClient.EXPECT().GetEvent("").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
checkNewUser(m)
}
func TestNewUserAuthRefreshFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token")).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUserUnlockFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, errors.New("bad password"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUserUnlockAddressesFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(errors.New("bad password"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
checkNewUser(m)
}
func checkNewUser(m mocks) {
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
defer cleanUpUserData(user)
_ = user.init(nil, m.pmapiClient)
waitForEvents()
a.Equal(m.t, testCredentials, user.creds)
}
func checkNewUserDisconnected(m mocks) {
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
defer cleanUpUserData(user)
_ = user.init(nil, m.pmapiClient)
waitForEvents()
a.Equal(m.t, testCredentialsDisconnected, user.creds)
}
func _TestUserEventRefreshUpdatesAddresses(t *testing.T) { // nolint[funlen]
a.Fail(t, "not implemented")
}

View File

@ -0,0 +1,113 @@
// Copyright (c) 2020 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
import (
"testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testNewUser(m mocks) *User {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
// Expectations for initial sync (when loading existing user from credentials store).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
assert.NoError(m.t, err)
err = user.init(nil, m.pmapiClient)
assert.NoError(m.t, err)
return user
}
func testNewUserForLogout(m mocks) *User {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
// These may or may not be hit depending on how fast the log out happens.
m.pmapiClient.EXPECT().SetAuths(nil).AnyTimes()
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil).AnyTimes()
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}).AnyTimes()
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
assert.NoError(m.t, err)
err = user.init(nil, m.pmapiClient)
assert.NoError(m.t, err)
return user
}
func cleanUpUserData(u *User) {
_ = u.clearStore()
}
func _TestNeverLongStorePath(t *testing.T) { // nolint[unused]
assert.Fail(t, "not implemented")
}
func TestClearStoreWithStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
require.Nil(t, user.store.Close())
user.store = nil
assert.Nil(t, user.clearStore())
}
func TestClearStoreWithoutStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
assert.NotNil(t, user.store)
assert.Nil(t, user.clearStore())
}

View File

@ -0,0 +1,41 @@
// Copyright (c) 2020 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
import (
"fmt"
"runtime"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
// UpdateCurrentUserAgent updates user agent on pmapi so each request has this
// information in headers for statistic purposes.
func UpdateCurrentUserAgent(bridgeVersion, os, clientName, clientVersion string) {
if os == "" {
os = runtime.GOOS
}
mailClient := "unknown client"
if clientName != "" {
mailClient = clientName
if clientVersion != "" {
mailClient += "/" + clientVersion
}
}
pmapi.CurrentUserAgent = fmt.Sprintf("Bridge/%s (%s; %s)", bridgeVersion, os, mailClient)
}

View File

@ -0,0 +1,51 @@
// Copyright (c) 2020 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
import (
"runtime"
"testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/stretchr/testify/assert"
)
func TestUpdateCurrentUserAgentGOOS(t *testing.T) {
UpdateCurrentUserAgent("ver", "", "", "")
assert.Equal(t, "Bridge/ver ("+runtime.GOOS+"; unknown client)", pmapi.CurrentUserAgent)
}
func TestUpdateCurrentUserAgentOS(t *testing.T) {
UpdateCurrentUserAgent("ver", "os", "", "")
assert.Equal(t, "Bridge/ver (os; unknown client)", pmapi.CurrentUserAgent)
}
func TestUpdateCurrentUserAgentClientVer(t *testing.T) {
UpdateCurrentUserAgent("ver", "os", "", "cver")
assert.Equal(t, "Bridge/ver (os; unknown client)", pmapi.CurrentUserAgent)
}
func TestUpdateCurrentUserAgentClientName(t *testing.T) {
UpdateCurrentUserAgent("ver", "os", "mail", "")
assert.Equal(t, "Bridge/ver (os; mail)", pmapi.CurrentUserAgent)
}
func TestUpdateCurrentUserAgentClientNameAndVersion(t *testing.T) {
UpdateCurrentUserAgent("ver", "os", "mail", "cver")
assert.Equal(t, "Bridge/ver (os; mail/cver)", pmapi.CurrentUserAgent)
}

53
internal/events/events.go Normal file
View File

@ -0,0 +1,53 @@
// Copyright (c) 2020 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 events provides names of events used by the event listener in bridge.
package events
import (
"time"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
// Constants of events used by the event listener in bridge.
const (
ErrorEvent = "error"
CloseConnectionEvent = "closeConnection"
LogoutEvent = "logout"
AddressChangedEvent = "addressChanged"
AddressChangedLogoutEvent = "addressChangedLogout"
UserRefreshEvent = "userRefresh"
RestartBridgeEvent = "restartBridge"
InternetOffEvent = "internetOff"
InternetOnEvent = "internetOn"
SecondInstanceEvent = "secondInstance"
OutgoingNoEncEvent = "outgoingNoEncryption"
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
UpgradeApplicationEvent = "upgradeApplication"
TLSCertIssue = "tlsCertPinningIssue"
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
LogoutEventTimeout = 3 * time.Minute
)
// SetupEvents specific to event type and data.
func SetupEvents(listener listener.Listener) {
listener.SetLimit(LogoutEvent, LogoutEventTimeout)
listener.SetBuffer(TLSCertIssue)
listener.SetBuffer(ErrorEvent)
}

View File

@ -0,0 +1,108 @@
// Copyright (c) 2020 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/>.
// +build darwin
package autoconfig
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
mobileconfig "github.com/ProtonMail/go-apple-mobileconfig"
)
func init() { //nolint[gochecknoinit]
available = append(available, &appleMail{})
}
type appleMail struct{}
func (c *appleMail) Name() string {
return "Apple Mail"
}
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.BridgeUser, addressIndex int) error { //nolint[funlen]
var addresses string
var displayName string
if user.IsCombinedAddressMode() {
displayName = user.GetPrimaryAddress()
addresses = strings.Join(user.GetAddresses(), ",")
} else {
for idx, address := range user.GetAddresses() {
if idx == addressIndex {
displayName = address
break
}
}
addresses = displayName
}
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
mc := &mobileconfig.Config{
EmailAddress: addresses,
DisplayName: displayName,
Identifier: "protonmail " + displayName + timestamp,
Imap: &mobileconfig.Imap{
Hostname: bridge.Host,
Port: imapPort,
Tls: imapSSL,
Username: displayName,
Password: user.GetBridgePassword(),
},
Smtp: &mobileconfig.Smtp{
Hostname: bridge.Host,
Port: smtpPort,
Tls: smtpSSL,
Username: displayName,
},
}
dir, err := ioutil.TempDir("", "protonmail-autoconfig")
if err != nil {
return err
}
// Make sure the temporary file is deleted.
go (func() {
<-time.After(10 * time.Minute)
_ = os.RemoveAll(dir)
})()
// Make sure the file is only readable for the current user.
f, err := os.OpenFile(filepath.Join(dir, "protonmail.mobileconfig"), os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return err
}
if err := mc.WriteTo(f); err != nil {
_ = f.Close()
return err
}
_ = f.Close()
return exec.Command("open", f.Name()).Run() // nolint[gosec]
}

View File

@ -0,0 +1,33 @@
// Copyright (c) 2020 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 autoconfig provides automatic config of IMAP and SMTP.
// For now only for Apple Mail.
package autoconfig
import "github.com/ProtonMail/proton-bridge/internal/frontend/types"
type AutoConfig interface {
Name() string
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.BridgeUser, addressIndex int) error
}
var available []AutoConfig //nolint[gochecknoglobals]
func Available() []AutoConfig {
return available
}

View File

@ -0,0 +1,100 @@
// Copyright (c) 2020 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 cli
import (
"fmt"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/abiosoft/ishell"
)
// completeUsernames is a helper to complete usernames as the user types.
func (f *frontendCLI) completeUsernames(args []string) (usernames []string) {
if len(args) > 1 {
return
}
arg := ""
if len(args) == 1 {
arg = args[0]
}
for _, user := range f.bridge.GetUsers() {
if strings.HasPrefix(strings.ToLower(user.Username()), strings.ToLower(arg)) {
usernames = append(usernames, user.Username())
}
}
return
}
// noAccountWrapper is a decorator for functions which need any account to be properly functional.
func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ishell.Context) {
return func(c *ishell.Context) {
users := f.bridge.GetUsers()
if len(users) == 0 {
f.Println("No active accounts. Please add account to continue.")
} else {
callback(c)
}
}
}
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
user := f.getUserByIndexOrName("")
if user != nil {
return user
}
numberOfAccounts := len(f.bridge.GetUsers())
indexRange := fmt.Sprintf("number between 0 and %d", numberOfAccounts-1)
if len(c.Args) == 0 {
f.Printf("Please choose %s or username.\n", indexRange)
return nil
}
arg := c.Args[0]
user = f.getUserByIndexOrName(arg)
if user == nil {
f.Printf("Wrong input '%s'. Choose %s or username.\n", bold(arg), indexRange)
return nil
}
return user
}
func (f *frontendCLI) getUserByIndexOrName(arg string) types.BridgeUser {
users := f.bridge.GetUsers()
numberOfAccounts := len(users)
if numberOfAccounts == 0 {
return nil
}
if numberOfAccounts == 1 {
return users[0]
}
if index, err := strconv.Atoi(arg); err == nil {
if index < 0 || index >= numberOfAccounts {
return nil
}
return users[index]
}
for _, user := range users {
if user.Username() == arg {
return user
}
}
return nil
}

View File

@ -0,0 +1,219 @@
// Copyright (c) 2020 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 cli
import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) listAccounts(c *ishell.Context) {
spacing := "%-2d: %-20s (%-15s, %-15s)\n"
f.Printf(bold(strings.Replace(spacing, "d", "s", -1)), "#", "account", "status", "address mode")
for idx, user := range f.bridge.GetUsers() {
connected := "disconnected"
if user.IsConnected() {
connected = "connected"
}
mode := "split"
if user.IsCombinedAddressMode() {
mode = "combined"
}
f.Printf(spacing, idx, user.Username(), connected, mode)
}
f.Println()
}
func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
if !user.IsConnected() {
f.Printf("Please login to %s to get email client configuration.\n", bold(user.Username()))
return
}
if user.IsCombinedAddressMode() {
f.showAccountAddressInfo(user, user.GetPrimaryAddress())
} else {
for _, address := range user.GetAddresses() {
f.showAccountAddressInfo(user, address)
}
}
}
func (f *frontendCLI) showAccountAddressInfo(user types.BridgeUser, address string) {
smtpSecurity := "STARTTLS"
if f.preferences.GetBool(preferences.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),
address,
user.GetBridgePassword(),
"STARTTLS",
)
f.Println("")
f.Printf("SMTP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
bridge.Host,
f.preferences.GetInt(preferences.SMTPPortKey),
address,
user.GetBridgePassword(),
smtpSecurity,
)
f.Println("")
}
func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
f.ShowPrompt(false)
defer f.ShowPrompt(true)
loginName := ""
if len(c.Args) > 0 {
user := f.getUserByIndexOrName(c.Args[0])
if user != nil {
loginName = user.GetPrimaryAddress()
}
}
if loginName == "" {
loginName = f.readStringInAttempts("Username", c.ReadLine, isNotEmpty)
if loginName == "" {
return
}
} else {
f.Println("Username:", loginName)
}
password := f.readStringInAttempts("Password", c.ReadPassword, isNotEmpty)
if password == "" {
return
}
f.Println("Authenticating ... ")
client, auth, err := f.bridge.Login(loginName, password)
if err != nil {
f.processAPIError(err)
return
}
if auth.HasTwoFactor() {
twoFactor := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
if twoFactor == "" {
return
}
_, err = client.Auth2FA(twoFactor, auth)
if err != nil {
f.processAPIError(err)
return
}
}
mailboxPassword := password
if auth.HasMailboxPassword() {
mailboxPassword = f.readStringInAttempts("Mailbox password", c.ReadPassword, isNotEmpty)
}
if mailboxPassword == "" {
return
}
f.Println("Adding account ...")
user, err := f.bridge.FinishLogin(client, auth, mailboxPassword)
if err != nil {
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
f.Println("Adding account was unsuccessful:", err)
return
}
f.Printf("Account %s was added successfully.\n", bold(user.Username()))
}
func (f *frontendCLI) logoutAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
if f.yesNoQuestion("Are you sure you want to logout account " + bold(user.Username())) {
if err := user.Logout(); err != nil {
f.printAndLogError("Logging out failed: ", err)
}
}
}
func (f *frontendCLI) deleteAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
if f.yesNoQuestion("Are you sure you want to " + bold("remove account "+user.Username())) {
clearCache := f.yesNoQuestion("Do you want to remove cache for this account")
if err := f.bridge.DeleteUser(user.ID(), clearCache); err != nil {
f.printAndLogError("Cannot delete account: ", err)
return
}
}
}
func (f *frontendCLI) deleteAccounts(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
if !f.yesNoQuestion("Do you really want remove all accounts") {
return
}
for _, user := range f.bridge.GetUsers() {
if err := f.bridge.DeleteUser(user.ID(), false); err != nil {
f.printAndLogError("Cannot delete account ", user.Username(), ": ", err)
}
}
c.Println("Keychain cleared")
}
func (f *frontendCLI) changeMode(c *ishell.Context) {
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
newMode := "combined mode"
if user.IsCombinedAddressMode() {
newMode = "split mode"
}
if !f.yesNoQuestion("Are you sure you want to change the mode for account " + bold(user.Username()) + " to " + bold(newMode)) {
return
}
if err := user.SwitchAddressMode(); err != nil {
f.printAndLogError("Cannot switch address mode:", err)
}
f.Printf("Address mode for account %s changed to %s\n", user.Username(), newMode)
}

View File

@ -0,0 +1,264 @@
// Copyright (c) 2020 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 cli provides CLI interface of the Bridge.
package cli
import (
"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/listener"
"github.com/abiosoft/ishell"
)
var (
log = config.GetLogEntry("frontend/cli") //nolint[gochecknoglobals]
)
type frontendCLI struct {
*ishell.Shell
config *config.Config
preferences *config.Preferences
eventListener listener.Listener
updates types.Updater
bridge types.Bridger
appRestart bool
}
// New returns a new CLI frontend configured with the given options.
func New( //nolint[funlen]
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
eventListener listener.Listener,
updates types.Updater,
bridge types.Bridger,
) *frontendCLI { //nolint[golint]
fe := &frontendCLI{
Shell: ishell.New(),
config: config,
preferences: preferences,
eventListener: eventListener,
updates: updates,
bridge: bridge,
appRestart: false,
}
// Clear commands.
clearCmd := &ishell.Cmd{Name: "clear",
Help: "remove stored accounts and preferences. (alias: cl)",
Aliases: []string{"cl"},
}
clearCmd.AddCmd(&ishell.Cmd{Name: "cache",
Help: "remove stored preferences for accounts (aliases: c, prefs, preferences)",
Aliases: []string{"c", "prefs", "preferences"},
Func: fe.deleteCache,
})
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: k, keychain)",
Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts,
})
fe.AddCmd(clearCmd)
// Change commands.
changeCmd := &ishell.Cmd{Name: "change",
Help: "change server or account settings (aliases: ch, switch)",
Aliases: []string{"ch", "switch"},
}
changeCmd.AddCmd(&ishell.Cmd{Name: "mode",
Help: "switch between combined addresses and split addresses mode for account. Use index or account name as parameter. (alias: m)",
Aliases: []string{"m"},
Func: fe.changeMode,
Completer: fe.completeUsernames,
})
changeCmd.AddCmd(&ishell.Cmd{Name: "port",
Help: "change port numbers of IMAP and SMTP servers. (alias: p)",
Aliases: []string{"p"},
Func: fe.changePort,
})
changeCmd.AddCmd(&ishell.Cmd{Name: "proxy",
Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.toggleAllowProxy,
})
changeCmd.AddCmd(&ishell.Cmd{Name: "smtp-security",
Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)",
Aliases: []string{"ssl", "starttls"},
Func: fe.changeSMTPSecurity,
})
fe.AddCmd(changeCmd)
// Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
Help: "check for Bridge updates. (aliases: u, v, version)",
Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates,
})
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"},
Func: fe.checkInternetConnection,
})
fe.AddCmd(checkCmd)
// Print info commands.
fe.AddCmd(&ishell.Cmd{Name: "log-dir",
Help: "print path to directory with logs. (aliases: log, logs)",
Aliases: []string{"log", "logs"},
Func: fe.printLogDir,
})
fe.AddCmd(&ishell.Cmd{Name: "manual",
Help: "print URL with instructions. (alias: man)",
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,
})
// Account commands.
fe.AddCmd(&ishell.Cmd{Name: "list",
Help: "print the list of accounts. (aliases: l, ls)",
Func: fe.noAccountWrapper(fe.listAccounts),
Aliases: []string{"l", "ls"},
})
fe.AddCmd(&ishell.Cmd{Name: "info",
Help: "print the configuration for account. Use index or account name as parameter. (alias: i)",
Func: fe.noAccountWrapper(fe.showAccountInfo),
Completer: fe.completeUsernames,
Aliases: []string{"i"},
})
fe.AddCmd(&ishell.Cmd{Name: "login",
Help: "login procedure to add or connect account. Optionally use index or account as parameter. (aliases: a, add, con, connect)",
Func: fe.loginAccount,
Aliases: []string{"add", "a", "con", "connect"},
Completer: fe.completeUsernames,
})
fe.AddCmd(&ishell.Cmd{Name: "logout",
Help: "disconnect the account. Use index or account name as parameter. (aliases: d, disconnect)",
Func: fe.noAccountWrapper(fe.logoutAccount),
Aliases: []string{"d", "disconnect"},
Completer: fe.completeUsernames,
})
fe.AddCmd(&ishell.Cmd{Name: "delete",
Help: "remove the account from keychain. Use index or account name as parameter. (aliases: del, rm, remove)",
Func: fe.noAccountWrapper(fe.deleteAccount),
Aliases: []string{"del", "rm", "remove"},
Completer: fe.completeUsernames,
})
// System commands.
fe.AddCmd(&ishell.Cmd{Name: "restart",
Help: "restart the bridge.",
Func: fe.restart,
})
go func() {
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)
internetOffCh := f.getEventChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent)
addressChangedCh := f.getEventChannel(events.AddressChangedEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue)
for {
select {
case errorDetails := <-errorCh:
f.Println("Bridge failed:", errorDetails)
case <-internetOffCh:
f.notifyInternetOff()
case <-internetOnCh:
f.notifyInternetOn()
case address := <-addressChangedCh:
f.Printf("Address changed for %s. You may need to reconfigure your email client.", address)
case address := <-addressChangedLogoutCh:
f.notifyLogout(address)
case userID := <-logoutCh:
user, err := f.bridge.GetUser(userID)
if err != nil {
return
}
f.notifyLogout(user.Username())
case <-certIssue:
f.notifyCertIssue()
}
}
}
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
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
}
f.preferences.SetBool(preferences.FirstStartKey, false)
f.Print(`
Welcome to ProtonMail Bridge interactive shell
___....___
^^ __..-:'':__:..:__:'':-..__
_.-:__:.-:'': : : :'':-.:__:-._
.':.-: : : : : : : : : :._:'.
_ :.': : : : : : : : : : : :'.: _
[ ]: : : : : : : : : : : : : :[ ]
[ ]: : : : : : : : : : : : : :[ ]
:::::::::[ ]:__:__:__:__:__:__:__:__:__:__:__:__:__:[ ]:::::::::::
!!!!!!!!![ ]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![ ]!!!!!!!!!!!
^^^^^^^^^[ ]^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[ ]^^^^^^^^^^^
[ ] [ ]
[ ] [ ]
jgs [ ] [ ]
~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^
`)
f.Run()
return nil
}

View File

@ -0,0 +1,164 @@
// Copyright (c) 2020 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 cli
import (
"fmt"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/connection"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/abiosoft/ishell"
)
var (
currentPort = "" //nolint[gochecknoglobals]
)
func (f *frontendCLI) restart(c *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to restart the Bridge") {
f.Println("Restarting Bridge...")
f.appRestart = true
f.Stop()
}
}
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if connection.CheckInternetConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact server please check you internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) {
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
}
func (f *frontendCLI) printManual(c *ishell.Context) {
f.Println("More instructions about the Bridge can be found at\n\n https://protonmail.com/bridge")
}
func (f *frontendCLI) deleteCache(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
if !f.yesNoQuestion("Do you really want to remove all stored preferences") {
return
}
if err := f.bridge.ClearData(); err != nil {
f.printAndLogError("Cache clear failed: ", err.Error())
return
}
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.Stop()
}
func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
isSSL := f.preferences.GetBool(preferences.SMTPSSLKey)
newSecurity := "SSL"
if isSSL {
newSecurity = "STARTTLS"
}
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.Println("Restarting Bridge...")
f.appRestart = true
f.Stop()
}
}
func (f *frontendCLI) changePort(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
currentPort = f.preferences.Get(preferences.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)
newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
if newSMTPPort == "" {
newSMTPPort = currentPort
}
smtpPortChanged := newSMTPPort != currentPort
if newIMAPPort == newSMTPPort {
f.Println("SMTP and IMAP ports must be different!")
return
}
if imapPortChanged || smtpPortChanged {
f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort)
f.preferences.Set(preferences.IMAPPortKey, newIMAPPort)
f.preferences.Set(preferences.SMTPPortKey, newSMTPPort)
f.Println("Restarting Bridge...")
f.appRestart = true
f.Stop()
} else {
f.Println("Nothing changed")
}
}
func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
if f.preferences.GetBool(preferences.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)
bridge.DisallowDoH()
}
} 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)
bridge.AllowDoH()
}
}
}
func (f *frontendCLI) isPortFree(port string) bool {
port = strings.Replace(port, ":", "", -1)
if port == "" || port == currentPort {
return true
}
number, err := strconv.Atoi(port)
if err != nil || number < 0 || number > 65535 {
f.Println("Input", port, "is not a valid port number.")
return false
}
if !ports.IsPortFree(number) {
f.Println("Port", number, "is occupied by another process.")
return false
}
return true
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2020 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 cli
import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
isUpToDate, latestVersionInfo, err := f.updates.CheckIsBridgeUpToDate()
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)
}
}
func (f *frontendCLI) printCredits(c *ishell.Context) {
for _, pkg := range strings.Split(bridge.Credits, ";") {
f.Println(pkg)
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) 2020 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 cli
import (
"strings"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/fatih/color"
)
const (
maxInputRepeat = 2
)
var (
bold = color.New(color.Bold).SprintFunc() //nolint[gochecknoglobals]
)
func isNotEmpty(val string) bool {
return val != ""
}
func (f *frontendCLI) yesNoQuestion(question string) bool {
f.Print(question, "? yes/"+bold("no")+": ")
yes := "yes"
answer := strings.ToLower(f.ReadLine())
for i := 0; i < len(answer); i++ {
if i >= len(yes) || answer[i] != yes[i] {
return false // Everything else is false.
}
}
return len(answer) > 0 // Empty is false.
}
func (f *frontendCLI) readStringInAttempts(title string, readFunc func() string, isOK func(string) bool) (value string) {
f.Printf("%s: ", title)
value = readFunc()
title = strings.ToLower(string(title[0])) + title[1:]
for i := 0; !isOK(value); i++ {
if i >= maxInputRepeat {
f.Println("Too many attempts")
return ""
}
f.Printf("Please fill %s: ", title)
value = readFunc()
}
return
}
func (f *frontendCLI) printAndLogError(args ...interface{}) {
log.Error(args...)
f.Println(args...)
}
func (f *frontendCLI) processAPIError(err error) {
log.Warn("API error: ", err)
switch err {
case pmapi.ErrAPINotReachable:
f.notifyInternetOff()
case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade()
default:
f.Println("Server error:", err.Error())
}
}
func (f *frontendCLI) notifyInternetOff() {
f.Println("Internet connection is not available.")
}
func (f *frontendCLI) notifyInternetOn() {
f.Println("Internet connection is available again.")
}
func (f *frontendCLI) notifyLogout(address string) {
f.Printf("Account %s is disconnected. Login to continue using this account with email client.", address)
}
func (f *frontendCLI) notifyNeedUpgrade() {
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
}
func (f *frontendCLI) notifyCredentialsError() {
// 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")
f.Println("and restart the application.")
}
func (f *frontendCLI) notifyCertIssue() {
// Print in 80-column width.
f.Println(`Connection security error: Your network connection to Proton services may
be insecure.
Description:
ProtonMail Bridge was not able to establish a secure connection to Proton
servers due to a TLS certificate error. This means your connection may
potentially be insecure and susceptible to monitoring by third parties.
Recommendation:
* If you trust your network operator, you can continue to use ProtonMail
as usual.
* If you don't trust your network operator, reconnect to ProtonMail over a VPN
(such as ProtonVPN) which encrypts your Internet connection, or use
a different network to access ProtonMail.
`)
}

View File

@ -0,0 +1,87 @@
// Copyright (c) 2020 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 frontend provides all interfaces of the Bridge.
package frontend
import (
"github.com/0xAX/notificator"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
var (
log = config.GetLogEntry("frontend") // nolint[unused]
)
// 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() {
notify := notificator.New(notificator.Options{
DefaultIcon: "../frontend/ui/icon/icon.png",
AppName: "ProtonMail Bridge",
})
_ = notify.Push("Fatal Error", "The ProtonMail Bridge has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
}
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
func New(
version,
buildVersion,
frontendType string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
eventListener listener.Listener,
updates types.Updater,
bridge *bridge.Bridge,
noEncConfirmator types.NoEncConfirmator,
) Frontend {
bridgeWrap := types.NewBridgeWrap(bridge)
return new(version, buildVersion, frontendType, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridgeWrap, noEncConfirmator)
}
func new(
version,
buildVersion,
frontendType string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
eventListener listener.Listener,
updates types.Updater,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
) Frontend {
switch frontendType {
case "cli":
return cli.New(panicHandler, config, preferences, eventListener, updates, bridge)
default:
return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
}
}

View File

@ -0,0 +1,430 @@
// Copyright (c) 2020 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/>.
import QtQuick 2.8
import ProtonUI 1.0
import BridgeUI 1.0
// NOTE: Keep the Column so the height and width is inherited from content
Column {
id: root
state: status
anchors.left: parent.left
property int row_width: 50 * Style.px
property int row_height: Style.accounts.heightAccount
property var listalias : aliases.split(";")
property int iAccount: index
Accessible.role: go.goos=="windows" ? Accessible.Grouping : Accessible.Row
Accessible.name: qsTr("Account %1, status %2", "Accessible text describing account row with arguments: account name and status (connected/disconnected), resp.").arg(account).arg(statusMark.text)
Accessible.description: Accessible.name
Accessible.ignored: !enabled || !visible
// Main row
Rectangle {
id: mainaccRow
anchors.left: parent.left
width : row_width
height : row_height
state: { return isExpanded ? "expanded" : "collapsed" }
color: Style.main.background
property string actionName : (
isExpanded ?
qsTr("Collapse row for account %2", "Accessible text of button showing additional configuration of account") :
qsTr("Expand row for account %2", "Accessible text of button hiding additional configuration of account")
). arg(account)
// override by other buttons
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked : {
if (root.state=="connected") {
mainaccRow.toggle_accountSettings()
}
}
cursorShape : root.state == "connected" ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true
onEntered: {
if (mainaccRow.state=="collapsed") {
mainaccRow.color = Qt.lighter(Style.main.background,1.1)
}
}
onExited: {
if (mainaccRow.state=="collapsed") {
mainaccRow.color = Style.main.background
}
}
}
// toggle down/up icon
Text {
id: toggleIcon
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
leftMargin : Style.main.leftMargin
}
color: Style.main.text
font {
pointSize : Style.accounts.sizeChevron * Style.pt
family : Style.fontawesome.name
}
text: Style.fa.chevron_down
MouseArea {
anchors.fill: parent
Accessible.role: Accessible.Button
Accessible.name: mainaccRow.actionName
Accessible.description: mainaccRow.actionName
Accessible.onPressAction : mainaccRow.toggle_accountSettings()
Accessible.ignored: root.state!="connected" || !root.enabled
}
}
// account name
TextMetrics {
id: accountMetrics
font : accountName.font
elide: Qt.ElideMiddle
elideWidth: Style.accounts.elideWidth
text: account
}
Text {
id: accountName
anchors {
verticalCenter : parent.verticalCenter
left : toggleIcon.left
leftMargin : Style.main.leftMargin
}
color: Style.main.text
font {
pointSize : (Style.main.fontSize+2*Style.px) * Style.pt
}
text: accountMetrics.elidedText
}
// status
ClickIconText {
id: statusMark
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : Style.accounts.leftMargin2
}
text : qsTr("connected", "status of a listed logged-in account")
iconText : Style.fa.circle_o
textColor : Style.main.textGreen
enabled : false
Accessible.ignored: true
}
// logout
ClickIconText {
id: logoutAccount
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : Style.accounts.leftMargin3
}
text : qsTr("Log out", "action to log out a connected account")
iconText : Style.fa.power_off
textBold : true
textColor : Style.main.textBlue
}
// remove
ClickIconText {
id: deleteAccount
anchors {
verticalCenter : parent.verticalCenter
right : parent.right
rightMargin : Style.main.rightMargin
}
text : qsTr("Remove", "deletes an account from the account settings page")
iconText : Style.fa.trash_o
textColor : Style.main.text
onClicked : {
dialogGlobal.input=root.iAccount
dialogGlobal.state="deleteUser"
dialogGlobal.show()
}
}
// functions
function toggle_accountSettings() {
if (root.state=="connected") {
if (mainaccRow.state=="collapsed" ) {
mainaccRow.state="expanded"
} else {
mainaccRow.state="collapsed"
}
}
}
states: [
State {
name: "collapsed"
PropertyChanges { target : toggleIcon ; text : root.state=="connected" ? Style.fa.chevron_down : " " }
PropertyChanges { target : accountName ; font.bold : false }
PropertyChanges { target : mainaccRow ; color : Style.main.background }
PropertyChanges { target : addressList ; visible : false }
},
State {
name: "expanded"
PropertyChanges { target : toggleIcon ; text : Style.fa.chevron_up }
PropertyChanges { target : accountName ; font.bold : true }
PropertyChanges { target : mainaccRow ; color : Style.accounts.backgroundExpanded }
PropertyChanges { target : addressList ; visible : true }
}
]
}
// List of adresses
Column {
id: addressList
anchors.left : parent.left
width: row_width
visible: false
property alias model : repeaterAddresses.model
Rectangle {
id: addressModeWrapper
anchors {
left : parent.left
right : parent.right
}
visible : mainaccRow.state=="expanded"
height : 2*Style.accounts.heightAddrRow/3
color : Style.accounts.backgroundExpanded
ClickIconText {
id: addressModeSwitch
anchors {
top : addressModeWrapper.top
right : addressModeWrapper.right
rightMargin : Style.main.rightMargin
}
textColor : Style.main.textBlue
iconText : Style.fa.exchange
iconOnRight : false
text : isCombinedAddressMode ?
qsTr("Switch to split addresses mode", "Text of button switching to mode with one configuration per each address.") :
qsTr("Switch to combined addresses mode", "Text of button switching to mode with one configuration for all addresses.")
onClicked: {
dialogGlobal.input=root.iAccount
dialogGlobal.state="addressmode"
dialogGlobal.show()
}
}
ClickIconText {
id: combinedAddressConfig
anchors {
top : addressModeWrapper.top
left : addressModeWrapper.left
leftMargin : Style.accounts.leftMarginAddr+Style.main.leftMargin
}
visible : isCombinedAddressMode
text : qsTr("Mailbox configuration", "Displays IMAP/SMTP settings information for a given account")
iconText : Style.fa.gear
textColor : Style.main.textBlue
onClicked : {
infoWin.showInfo(root.iAccount,0)
}
}
}
Repeater {
id: repeaterAddresses
model: ["one", "two"]
Rectangle {
id: addressRow
visible: !isCombinedAddressMode
anchors {
left : parent.left
right : parent.right
}
height: Style.accounts.heightAddrRow
color: Style.accounts.backgroundExpanded
// icon level down
Text {
id: levelDown
anchors {
left : parent.left
leftMargin : Style.accounts.leftMarginAddr
verticalCenter : wrapAddr.verticalCenter
}
text : Style.fa.level_up
font.family : Style.fontawesome.name
color : Style.main.textDisabled
rotation : 90
}
Rectangle {
id: wrapAddr
anchors {
top : parent.top
left : levelDown.right
right : parent.right
leftMargin : Style.main.leftMargin
rightMargin : Style.main.rightMargin
}
height: Style.accounts.heightAddr
border {
width : Style.main.border
color : Style.main.line
}
color: Style.accounts.backgroundAddrRow
TextMetrics {
id: addressMetrics
font: address.font
elideWidth: 2*wrapAddr.width/3
elide: Qt.ElideMiddle
text: modelData
}
Text {
id: address
anchors {
verticalCenter : parent.verticalCenter
left: parent.left
leftMargin: Style.main.leftMargin
}
font.pointSize : Style.main.fontSize * Style.pt
color: Style.main.text
text: addressMetrics.elidedText
}
ClickIconText {
id: addressConfig
anchors {
verticalCenter : parent.verticalCenter
right: parent.right
rightMargin: Style.main.rightMargin
}
text : qsTr("Address configuration", "Display the IMAP/SMTP configuration for address")
iconText : Style.fa.gear
textColor : Style.main.textBlue
onClicked : infoWin.showInfo(root.iAccount,index)
Accessible.description: qsTr("Address configuration for %1", "Accessible text of button displaying the IMAP/SMTP configuration for address %1").arg(modelData)
Accessible.ignored: !enabled
}
MouseArea {
id: clickSettings
anchors.fill: wrapAddr
onClicked : addressConfig.clicked()
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onPressed: {
wrapAddr.color = Qt.rgba(1,1,1,0.20)
}
onEntered: {
wrapAddr.color = Qt.rgba(1,1,1,0.15)
}
onExited: {
wrapAddr.color = Style.accounts.backgroundAddrRow
}
}
}
}
}
}
Rectangle {
id: line
color: Style.accounts.line
height: Style.accounts.heightLine
width: root.row_width
}
states: [
State {
name: "connected"
PropertyChanges {
target : addressList
model : listalias
}
PropertyChanges {
target : toggleIcon
color : Style.main.text
}
PropertyChanges {
target : accountName
color : Style.main.text
}
PropertyChanges {
target : statusMark
textColor : Style.main.textGreen
text : qsTr("connected", "status of a listed logged-in account")
iconText : Style.fa.circle
}
PropertyChanges {
target : logoutAccount
text : qsTr("Log out", "action to log out a connected account")
onClicked : {
mainaccRow.state="collapsed"
dialogGlobal.input = root.iAccount
dialogGlobal.state = "logout"
dialogGlobal.show()
dialogGlobal.confirmed()
}
}
},
State {
name: "disconnected"
PropertyChanges {
target : addressList
model : 0
}
PropertyChanges {
target : toggleIcon
color : Style.main.textDisabled
}
PropertyChanges {
target : accountName
color : Style.main.textDisabled
}
PropertyChanges {
target : statusMark
textColor : Style.main.textDisabled
text : qsTr("disconnected", "status of a listed logged-out account")
iconText : Style.fa.circle_o
}
PropertyChanges {
target : logoutAccount
text : qsTr("Log in", "action to log in a disconnected account")
onClicked : {
dialogAddUser.username = root.listalias[0]
dialogAddUser.show()
dialogAddUser.inputPassword.focusInput = true
}
}
}
]
}

View File

@ -0,0 +1,72 @@
// Copyright (c) 2020 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/>.
// Dialog with main menu
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Rectangle {
id: root
color: "#aaff5577"
anchors {
left : tabbar.left
right : tabbar.right
top : tabbar.bottom
bottom : parent.bottom
}
visible: false
MouseArea {
anchors.fill: parent
onClicked: toggle()
}
Rectangle {
color : Style.menu.background
radius : Style.menu.radius
width : Style.menu.width
height : Style.menu.height
anchors {
top : parent.top
right : parent.right
topMargin : Style.menu.topMargin
rightMargin : Style.menu.rightMargin
}
MouseArea {
anchors.fill: parent
}
Text {
anchors.centerIn: parent
text: qsTr("About")
color: Style.menu.text
}
}
function toggle(){
if (root.visible == false) {
root.visible = true
} else {
root.visible = false
}
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) 2020 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/>.
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Item {
Rectangle {
anchors.centerIn: parent
width: Style.main.width
height: 3*Style.main.height/4
color: "transparent"
//color: "red"
ListView {
anchors.fill: parent
clip : true
model : go.credits.split(";")
delegate: AccessibleText {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData
color: Style.main.text
font.pointSize : Style.main.fontSize * Style.pt
}
footer: ButtonRounded {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Close", "close window")
onClicked: dialogCredits.hide()
}
}
}
}

View File

@ -0,0 +1,124 @@
// Copyright (c) 2020 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/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import ProtonUI 1.0
Dialog {
id: root
title : ""
isDialogBusy: false
property string firstParagraph : qsTr("The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer.", "instructions that appear on welcome screen at first start")
property string secondParagraph : qsTr("To add your ProtonMail account to the Bridge and <strong>generate your Bridge password</strong>, please see <a href=\"https://protonmail.com/bridge/install\">the installation guide</a> for detailed setup instructions.", "confirms and dismisses a notification (URL that leads to installation guide should stay intact)")
Column {
id: dialogMessage
property int heightInputs : welcome.height + middleSep.height + instructions.height + buttSep.height + buttonOkay.height + imageSep.height + logo.height
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogMessage.heightInputs)/2 }
Text {
id:welcome
color: Style.main.text
font.bold: true
font.pointSize: 1.5*Style.main.fontSize*Style.pt
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
text: qsTr("Welcome to the", "welcome screen that appears on first start")
}
Rectangle {id: imageSep; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator }
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
Image {
id: logo
anchors.bottom : pmbridge.baseline
height : 2*Style.main.fontSize
fillMode : Image.PreserveAspectFit
mipmap : true
source : "../ProtonUI/images/pm_logo.png"
}
AccessibleText {
id:pmbridge
color: Style.main.text
font.bold: true
font.pointSize: 2.2*Style.main.fontSize*Style.pt
horizontalAlignment: Text.AlignHCenter
text: qsTr("ProtonMail Bridge", "app title")
Accessible.name: this.clearText(pmbridge.text)
Accessible.description: this.clearText(welcome.text+ " " + pmbridge.text + ". " + root.firstParagraph + ". " + root.secondParagraph)
}
}
Rectangle { id:middleSep; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator }
Text {
id:instructions
color: Style.main.text
font.pointSize: Style.main.fontSize*Style.pt
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
width: root.width/1.5
wrapMode: Text.Wrap
textFormat: Text.RichText
text: "<html><style>a { color: "+Style.main.textBlue+"; text-decoration: none;}</style><body>"+
root.firstParagraph +
"<br/><br/>"+
root.secondParagraph +
"</body></html>"
onLinkActivated: {
Qt.openUrlExternally(link)
}
}
Rectangle { id:buttSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
ButtonRounded {
id:buttonOkay
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
fa_icon: Style.fa.check
text: qsTr("Okay", "confirms and dismisses a notification")
onClicked : root.hide()
anchors.horizontalCenter: parent.horizontalCenter
}
}
timer.interval : 3000
Connections {
target: timer
onTriggered: {
}
}
onShow : {
pmbridge.Accessible.selected = true
}
}

View File

@ -0,0 +1,233 @@
// Copyright (c) 2020 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/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
import QtQuick.Controls 2.2 as QC
Dialog {
id: root
title : "Set IMAP & SMTP settings"
subtitle : "Changes require reconfiguration of Mail client. (Bridge will automatically restart)"
isDialogBusy: currentIndex==1
Column {
id: dialogMessage
property int heightInputs : imapPort.height + middleSep.height + smtpPort.height + buttonSep.height + buttonRow.height + secSMTPSep.height + securitySMTP.height
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogMessage.heightInputs)/1.6 }
InputField {
id: imapPort
iconText : Style.fa.hashtag
label : qsTr("IMAP port", "entry field to choose port used for the IMAP server")
text : "undef"
}
Rectangle { id:middleSep; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator }
InputField {
id: smtpPort
iconText : Style.fa.hashtag
label : qsTr("SMTP port", "entry field to choose port used for the SMTP server")
text : "undef"
}
Rectangle { id:secSMTPSep; color : Style.transparent; width : Style.main.dummy; height : Style.dialog.heightSeparator }
// SSL button group
Rectangle {
anchors.horizontalCenter : parent.horizontalCenter
width : Style.dialog.widthInput
height : securitySMTPLabel.height + securitySMTP.height
color : "transparent"
AccessibleText {
id: securitySMTPLabel
anchors.left : parent.left
text:qsTr("SMTP connection mode")
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
}
QC.ButtonGroup {
buttons: securitySMTP.children
}
Row {
id: securitySMTP
spacing: Style.dialog.spacing
anchors.top: securitySMTPLabel.bottom
anchors.topMargin: Style.dialog.fontSize
CheckBoxLabel {
id: securitySMTPSSL
text: qsTr("SSL")
}
CheckBoxLabel {
checked: true
id: securitySMTPSTARTTLS
text: qsTr("STARTTLS")
}
}
}
Rectangle { id:buttonSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main: Style.dialog.text
fa_icon: Style.fa.times
text: qsTr("Cancel", "dismisses current action")
onClicked : root.hide()
}
ButtonRounded {
id: buttonYes
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
fa_icon: Style.fa.check
text: qsTr("Okay", "confirms and dismisses a notification")
onClicked : root.confirmed()
}
}
}
Column {
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-answ.height)/2 }
Text {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
width : parent.width/2
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
text : "IMAP: " + imapPort.text + "\nSMTP: " + smtpPort.text + "\nSMTP Connection Mode: " + getSelectedSSLMode() + "\n\n" +
qsTr("Settings will be applied after the next start. You will need to reconfigure your email client(s).", "after user changes their ports they will see this notification to reconfigure their setup") +
"\n\n" +
qsTr("Bridge will now restart.", "after user changes their ports this appears to notify the user of restart")
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
}
function areInputsOK() {
var isOK = true
var imapUnchanged = false
var secSMTPUnchanged = (securitySMTPSTARTTLS.checked == go.isSMTPSTARTTLS())
root.warning.text = ""
if (imapPort.text!=go.getIMAPPort()) {
if (go.isPortOpen(imapPort.text)!=0) {
imapPort.rightIcon = Style.fa.exclamation_triangle
root.warning.text = qsTr("Port number is not available.", "if the user changes one of their ports to a port that is occupied by another application")
isOK=false
} else {
imapPort.rightIcon = Style.fa.check_circle
}
} else {
imapPort.rightIcon = ""
imapUnchanged = true
}
if (smtpPort.text!=go.getSMTPPort()) {
if (go.isPortOpen(smtpPort.text)!=0) {
smtpPort.rightIcon = Style.fa.exclamation_triangle
root.warning.text = qsTr("Port number is not available.", "if the user changes one of their ports to a port that is occupied by another application")
isOK=false
} else {
smtpPort.rightIcon = Style.fa.check_circle
}
} else {
smtpPort.rightIcon = ""
if (imapUnchanged && secSMTPUnchanged) {
root.warning.text = qsTr("Please change at least one port number or SMTP security.", "if the user tries to change IMAP/SMTP ports to the same ports as before")
isOK=false
}
}
if (imapPort.text == smtpPort.text) {
smtpPort.rightIcon = Style.fa.exclamation_triangle
root.warning.text = qsTr("Port numbers must be different.", "if the user sets both the IMAP and SMTP ports to the same number")
isOK=false
}
root.warning.visible = !isOK
return isOK
}
function confirmed() {
if (areInputsOK()) {
incrementCurrentIndex()
timer.start()
}
}
function getSelectedSSLMode() {
if (securitySMTPSTARTTLS.checked == true) {
return "STARTTLS"
} else {
return "SSL"
}
}
onShow : {
imapPort.text = go.getIMAPPort()
smtpPort.text = go.getSMTPPort()
if (go.isSMTPSTARTTLS()) {
securitySMTPSTARTTLS.checked = true
} else {
securitySMTPSSL.checked = true
}
areInputsOK()
root.warning.visible = false
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
timer.interval : 3000
Connections {
target: timer
onTriggered: {
go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked)
go.isRestarting = true
Qt.quit()
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) 2020 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/>.
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Dialog {
id: root
title: qsTr("Connection security error", "Title of modal explainning TLS issue")
property string par1Title : qsTr("Description:", "Title of paragraph describing the issue")
property string par1Text : qsTr (
"ProtonMail Bridge was not able to establish a secure connection to Proton servers due to a TLS certificate error. "+
"This means your connection may potentially be insecure and susceptible to monitoring by third parties.",
"A paragraph describing the issue"
)
property string par2Title : qsTr("Recommendation:", "Title of paragraph describing recommended steps")
property string par2Text : qsTr (
"If you are on a corporate or public network, the network administrator may be monitoring or intercepting all traffic.",
"A paragraph describing network issue"
)
property string par2ul1 : qsTr(
"If you trust your network operator, you can continue to use ProtonMail as usual.",
"A list item describing recomendation for trusted network"
)
property string par2ul2 : qsTr(
"If you don't trust your network operator, reconnect to ProtonMail over a VPN (such as ProtonVPN) "+
"which encrypts your Internet connection, or use a different network to access ProtonMail.",
"A list item describing recomendation for untrusted network"
)
property string par3Text : qsTr("Learn more on our knowledge base article","A paragraph describing where to find more information")
property string kbArticleText : qsTr("What is TLS certificate error.", "Link text for knowledge base article")
property string kbArticleLink : "https://protonmail.com/support/knowledge-base/"
Item {
AccessibleText {
anchors.centerIn: parent
color: Style.old.pm_white
linkColor: color
width: parent.width - 50 * Style.px
wrapMode: Text.WordWrap
font.pointSize: Style.main.fontSize*Style.pt
onLinkActivated: Qt.openUrlExternally(link)
text: "<h3>"+par1Title+"</h3>"+
par1Text+"<br>\n"+
"<h3>"+par2Title+"</h3>"+
par2Text+
"<ul>"+
"<li>"+par2ul1+"</li>"+
"<li>"+par2ul2+"</li>"+
"</ul>"+"<br>\n"+
""
//par3Text+
//" <a href='"+kbArticleLink+"'>"+kbArticleText+"</a>\n"
}
}
}

View File

@ -0,0 +1,382 @@
// Copyright (c) 2020 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/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Dialog {
id: root
title : ""
property string input
property alias question : msg.text
property alias note : noteText.text
property alias answer : answ.text
property alias buttonYes : buttonYes
property alias buttonNo : buttonNo
isDialogBusy: currentIndex==1
signal confirmed()
Column {
id: dialogMessage
property int heightInputs : msg.height+
middleSep.height+
buttonRow.height +
(checkboxSep.visible ? checkboxSep.height : 0 ) +
(noteSep.visible ? noteSep.height : 0 ) +
(checkBoxWrapper.visible ? checkBoxWrapper.height : 0 ) +
(root.note!="" ? noteText.height : 0 )
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogMessage.heightInputs)/2 }
AccessibleText {
id:noteText
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: false
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Rectangle { id: noteSep; visible: note!=""; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
AccessibleText {
id: msg
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
width: 2*parent.width/3
text : ""
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Rectangle { id: checkboxSep; visible: checkBoxWrapper.visible; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
Row {
id: checkBoxWrapper
property bool isChecked : false
visible: root.state=="deleteUser"
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
function toggle() {
checkBoxWrapper.isChecked = !checkBoxWrapper.isChecked
}
Text {
id: checkbox
font {
pointSize : Style.dialog.iconSize * Style.pt
family : Style.fontawesome.name
}
anchors.verticalCenter : parent.verticalCenter
text: checkBoxWrapper.isChecked ? Style.fa.check_square_o : Style.fa.square_o
color: checkBoxWrapper.isChecked ? Style.main.textBlue : Style.main.text
MouseArea {
anchors.fill: parent
onPressed: checkBoxWrapper.toggle()
cursorShape: Qt.PointingHandCursor
}
}
Text {
id: checkBoxNote
anchors.verticalCenter : parent.verticalCenter
text: qsTr("Additionally delete all stored preferences and data", "when removing an account, this extra preference additionally deletes all cached data")
color: Style.main.text
font.pointSize: Style.dialog.fontSize * Style.pt
MouseArea {
anchors.fill: parent
onPressed: checkBoxWrapper.toggle()
cursorShape: Qt.PointingHandCursor
Accessible.role: Accessible.CheckBox
Accessible.checked: checkBoxWrapper.isChecked
Accessible.name: checkBoxNote.text
Accessible.description: checkBoxNote.text
Accessible.ignored: checkBoxNote.text == ""
Accessible.onToggleAction: checkBoxWrapper.toggle()
Accessible.onPressAction: checkBoxWrapper.toggle()
}
}
}
Rectangle { id: middleSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main: Style.dialog.text
fa_icon: Style.fa.times
text: qsTr("No")
onClicked : root.hide()
}
ButtonRounded {
id: buttonYes
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
fa_icon: Style.fa.check
text: qsTr("Yes")
onClicked : {
currentIndex=1
root.confirmed()
}
}
}
}
Column {
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-answ.height)/2 }
AccessibleText {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
color: Style.old.pm_white
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
width: 3*parent.width/4
horizontalAlignment: Text.AlignHCenter
text : qsTr("Waiting...", "in general this displays between screens when processing data takes a long time")
wrapMode: Text.Wrap
}
}
states : [
State {
name: "quit"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Close Bridge", "quits the application")
question : qsTr("Are you sure you want to close the Bridge?", "asked when user tries to quit the application")
note : ""
answer : qsTr("Closing Bridge...", "displayed when user is quitting application")
}
},
State {
name: "logout"
PropertyChanges {
target: root
currentIndex : 1
title : qsTr("Logout", "title of page that displays during account logout")
question : ""
note : ""
answer : qsTr("Logging out...", "displays during account logout")
}
},
State {
name: "deleteUser"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Delete account", "title of page that displays during account deletion")
question : qsTr("Are you sure you want to remove this account?", "displays during account deletion")
note : ""
answer : qsTr("Deleting ...", "displays during account deletion")
}
},
State {
name: "clearChain"
PropertyChanges {
target : root
currentIndex : 0
title : qsTr("Clear keychain", "title of page that displays during keychain clearing")
question : qsTr("Are you sure you want to clear your keychain?", "displays during keychain clearing")
note : qsTr("This will remove all accounts that you have added to the Bridge and disconnect you from your email client(s).", "displays during keychain clearing")
answer : qsTr("Clearing the keychain ...", "displays during keychain clearing")
}
},
State {
name: "clearCache"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Clear cache", "title of page that displays during cache clearing")
question : qsTr("Are you sure you want to clear your local cache?", "displays during cache clearing")
note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, temporarily slowing down the email download process significantly.", "displays during cache clearing")
answer : qsTr("Clearing the cache ...", "displays during cache clearing")
}
},
State {
name: "checkUpdates"
PropertyChanges {
target: root
currentIndex : 1
title : ""
question : ""
note : ""
answer : qsTr("Checking for updates ...", "displays if user clicks the Check for Updates button in the Help tab")
}
},
State {
name: "addressmode"
PropertyChanges {
target: root
currentIndex : 0
title : ""
question : qsTr("Do you want to continue?", "asked when the user changes between split and combined address mode")
note : qsTr("Changing between split and combined address mode will require you to delete your account(s) from your email client and begin the setup process from scratch.", "displayed when the user changes between split and combined address mode")
answer : qsTr("Configuring address mode...", "displayed when the user changes between split and combined address mode")
}
},
State {
name: "toggleAutoStart"
PropertyChanges {
target: root
currentIndex : 1
question : ""
note : ""
title : ""
answer : {
var msgTurnOn = qsTr("Turning on automatic start of Bridge...", "when the automatic start feature is selected")
var msgTurnOff = qsTr("Turning off automatic start of Bridge...", "when the automatic start feature is deselected")
return go.isAutoStart==false ? msgTurnOff : msgTurnOn
}
}
},
State {
name: "toggleAllowProxy"
PropertyChanges {
target: root
currentIndex : 0
question : {
var questionTurnOn = qsTr("Do you want to allow alternative routing?")
var questionTurnOff = qsTr("Do you want to disallow alternative routing?")
return go.isProxyAllowed==false ? questionTurnOn : questionTurnOff
}
note : qsTr("In case Proton sites are blocked, this setting allows Bridge to try alternative network routing to reach Proton, which can be useful for bypassing firewalls or network issues. We recommend keeping this setting on for greater reliability.")
title : {
var titleTurnOn = qsTr("Allow alternative routing")
var titleTurnOff = qsTr("Disallow alternative routing")
return go.isProxyAllowed==false ? titleTurnOn : titleTurnOff
}
answer : {
var msgTurnOn = qsTr("Allowing Bridge to use alternative routing to connect to Proton...", "when the allow proxy feature is selected")
var msgTurnOff = qsTr("Disallowing Bridge to use alternative routing to connect to Proton...", "when the allow proxy feature is deselected")
return go.isProxyAllowed==false ? msgTurnOn : msgTurnOff
}
}
},
State {
name: "noKeychain"
PropertyChanges {
target: root
currentIndex : 0
note : qsTr(
"%1 is not able to detected a supported password manager (pass, gnome-keyring). Please install and setup supported password manager and restart the application.",
"Error message when no keychain is detected"
).arg(go.programTitle)
question : qsTr("Do you want to close application now?", "when no password manager found." )
title : "No system password manager detected"
answer : qsTr("Closing Bridge...", "displayed when user is quitting application")
}
},
State {
name: "undef";
PropertyChanges {
target: root
currentIndex : 1
question : ""
note : ""
title : ""
answer : ""
}
}
]
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
onHide: {
checkBoxWrapper.isChecked = false
state = "undef"
}
onShow: {
// hide all other dialogs
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
} else {
timer.interval = 300
}
answ.forceActiveFocus()
timer.start()
}
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) }
}
}
Keys.onPressed: {
if (event.key == Qt.Key_Enter) {
root.confirmed()
}
}
}

View File

@ -0,0 +1,134 @@
// Copyright (c) 2020 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/>.
// List the settings
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Item {
id: root
// must have wrapper
Rectangle {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
color: Style.main.background
// content
Column {
anchors.horizontalCenter : parent.horizontalCenter
ButtonIconText {
id: logs
anchors.left: parent.left
text: qsTr("Logs", "title of button that takes user to logs directory")
leftIcon.text : Style.fa.align_justify
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: go.openLogs()
}
ButtonIconText {
id: bugreport
anchors.left: parent.left
text: qsTr("Report Bug", "title of button that takes user to bug report form")
leftIcon.text : Style.fa.bug
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: bugreportWin.show()
}
ButtonIconText {
id: manual
anchors.left: parent.left
text: qsTr("Setup Guide", "title of button that opens setup and installation guide")
leftIcon.text : Style.fa.book
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: go.openManual()
}
ButtonIconText {
id: updates
anchors.left: parent.left
text: qsTr("Check for Updates", "title of button to check for any app updates")
leftIcon.text : Style.fa.refresh
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: {
dialogGlobal.state="checkUpdates"
dialogGlobal.show()
dialogGlobal.confirmed()
}
}
// Bottom version notes
Rectangle {
anchors.horizontalCenter : parent.horizontalCenter
height: viewAccount.separatorNoAccount - 3.2*manual.height
width: wrapper.width
color : "transparent"
AccessibleText {
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
color: Style.main.textDisabled
horizontalAlignment: Qt.AlignHCenter
font.pointSize : Style.main.fontSize * Style.pt
text:
"ProtonMail Bridge "+go.getBackendVersion()+"\n"+
"© 2020 Proton Technologies AG"
}
}
Row {
anchors.left : parent.left
Rectangle { height: Style.dialog.spacing; width: (wrapper.width- credits.width - release.width - sepaCreditsRelease.width)/2; color: "transparent"}
ClickIconText {
id:credits
iconText : ""
text : qsTr("Credits", "link to click on to view list of credited libraries")
textColor : Style.main.textDisabled
fontSize : Style.main.fontSize
textUnderline : true
onClicked : winMain.dialogCredits.show()
}
Rectangle {id: sepaCreditsRelease ; height: Style.dialog.spacing; width: Style.main.dummy; color: "transparent"}
ClickIconText {
id:release
iconText : ""
text : qsTr("Release notes", "link to click on to view release notes for this version of the app")
textColor : Style.main.textDisabled
fontSize : Style.main.fontSize
textUnderline : true
onClicked : {
go.getLocalVersionInfo()
winMain.dialogVersionInfo.show()
}
}
}
}
}
}

View File

@ -0,0 +1,144 @@
// Copyright (c) 2020 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/>.
// Window for imap and smtp settings
import QtQuick 2.8
import QtQuick.Window 2.2
import BridgeUI 1.0
import ProtonUI 1.0
Window {
id:root
width : Style.info.width
height : Style.info.height
minimumWidth : Style.info.width
minimumHeight : Style.info.height
maximumWidth : Style.info.width
maximumHeight : Style.info.height
color: "transparent"
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
title : address
Accessible.role: Accessible.Window
Accessible.name: qsTr("Configuration information for %1").arg(address)
Accessible.description: Accessible.name
property QtObject accData : QtObject { // avoid null-pointer error
property string account : "undef"
property string aliases : "undef"
property string hostname : "undef"
property string password : "undef"
property int portIMAP : 0
property int portSMTP : 0
}
property string address : "undef"
property int indexAccount : 0
property int indexAddress : 0
WindowTitleBar {
id: titleBar
window: root
}
Rectangle { // background
color: Style.main.background
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
bottom : parent.bottom
}
border {
width: Style.main.border
color: Style.tabbar.background
}
}
// info content
Column {
anchors {
left: parent.left
top: titleBar.bottom
leftMargin: Style.main.leftMargin
topMargin: Style.info.topMargin
}
width : root.width - Style.main.leftMargin - Style.main.rightMargin
TextLabel { text: qsTr("IMAP SETTINGS", "title of the portion of the configuration screen that contains IMAP settings"); state: "heading" }
Rectangle { width: parent.width; height: Style.info.topMargin; color: "#00000000"}
Grid {
columns: 2
rowSpacing: Style.main.fontSize
TextLabel { text: qsTr("Hostname", "in configuration screen, displays the server hostname (127.0.0.1)") + ":"} TextValue { text: root.accData.hostname }
TextLabel { text: qsTr("Port", "in configuration screen, displays the server port (ex. 1025)") + ":"} TextValue { text: root.accData.portIMAP }
TextLabel { text: qsTr("Username", "in configuration screen, displays the username to use with the desktop client") + ":"} TextValue { text: root.address }
TextLabel { text: qsTr("Password", "in configuration screen, displays the Bridge password to use with the desktop client") + ":"} TextValue { text: root.accData.password }
TextLabel { text: qsTr("Security", "in configuration screen, displays the IMAP security settings") + ":"} TextValue { text: "STARTTLS" }
}
Rectangle { width: Style.main.dummy; height: Style.main.fontSize; color: "#00000000"}
Rectangle { width: Style.main.dummy; height: Style.info.topMargin; color: "#00000000"}
TextLabel { text: qsTr("SMTP SETTINGS", "title of the portion of the configuration screen that contains SMTP settings"); state: "heading" }
Rectangle { width: Style.main.dummy; height: Style.info.topMargin; color: "#00000000"}
Grid {
columns: 2
rowSpacing: Style.main.fontSize
TextLabel { text: qsTr("Hostname", "in configuration screen, displays the server hostname (127.0.0.1)") + ":"} TextValue { text: root.accData.hostname }
TextLabel { text: qsTr("Port", "in configuration screen, displays the server port (ex. 1025)") + ":"} TextValue { text: root.accData.portSMTP }
TextLabel { text: qsTr("Username", "in configuration screen, displays the username to use with the desktop client") + ":"} TextValue { text: root.address }
TextLabel { text: qsTr("Password", "in configuration screen, displays the Bridge password to use with the desktop client") + ":"} TextValue { text: root.accData.password }
TextLabel { text: qsTr("Security", "in configuration screen, displays the SMTP security settings") + ":"} TextValue { text: go.isSMTPSTARTTLS() ? "STARTTLS" : "SSL" }
}
Rectangle { width: Style.main.dummy; height: Style.main.fontSize; color: "#00000000"}
Rectangle { width: Style.main.dummy; height: Style.info.topMargin; color: "#00000000"}
}
// apple mail button
ButtonRounded{
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: Style.info.topMargin
}
color_main : Style.main.textBlue
isOpaque: false
text: qsTr("Configure Apple Mail", "button on configuration screen to automatically configure Apple Mail")
height: Style.main.fontSize*2
width: 2*parent.width/3
onClicked: {
go.configureAppleMail(root.indexAccount, root.indexAddress)
}
visible: go.goos == "darwin"
}
function showInfo(iAccount, iAddress) {
root.indexAccount = iAccount
root.indexAddress = iAddress
root.accData = accountsModel.get(iAccount)
root.address = accData.aliases.split(";")[iAddress]
root.show()
root.raise()
root.requestActivate()
}
function hide() {
root.visible = false
}
}

View File

@ -0,0 +1,455 @@
// Copyright (c) 2020 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/>.
// This is main window
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import BridgeUI 1.0
import ProtonUI 1.0
// Main Window
Window {
id: root
property alias tabbar : tabbar
property alias viewContent : viewContent
property alias viewAccount : viewAccount
property alias dialogAddUser : dialogAddUser
property alias dialogChangePort : dialogChangePort
property alias dialogCredits : dialogCredits
property alias dialogTlsCert : dialogTlsCert
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
property alias updateState : infoBar.state
property alias tlsBarState : tlsBar.state
property int heightContent : height-titleBar.height
// main window appeareance
width : Style.main.width
height : Style.main.height
flags : Qt.Window | Qt.FramelessWindowHint
color: go.goos=="windows" ? "black" : "transparent"
title: go.programTitle
minimumWidth: Style.main.width
minimumHeight: Style.main.height
maximumWidth: Style.main.width
property bool isOutdateVersion : root.updateState == "forceUpdate"
property bool activeContent :
!dialogAddUser .visible &&
!dialogChangePort .visible &&
!dialogCredits .visible &&
!dialogTlsCert .visible &&
!dialogUpdate .visible &&
!dialogFirstStart .visible &&
!dialogGlobal .visible &&
!dialogVersionInfo .visible &&
!bubbleNote .visible
Accessible.role: Accessible.Grouping
Accessible.description: qsTr("Window %1").arg(title)
Accessible.name: Accessible.description
Component.onCompleted : {
gui.winMain = root
console.log("GraphicsInfo of", titleBar,
"api" , titleBar.GraphicsInfo.api ,
"majorVersion" , titleBar.GraphicsInfo.majorVersion ,
"minorVersion" , titleBar.GraphicsInfo.minorVersion ,
"profile" , titleBar.GraphicsInfo.profile ,
"renderableType" , titleBar.GraphicsInfo.renderableType ,
"shaderCompilationType" , titleBar.GraphicsInfo.shaderCompilationType ,
"shaderSourceType" , titleBar.GraphicsInfo.shaderSourceType ,
"shaderType" , titleBar.GraphicsInfo.shaderType)
tabbar.focusButton()
}
WindowTitleBar {
id: titleBar
window: root
}
Rectangle {
anchors {
top : titleBar.bottom
left : parent.left
right : parent.right
bottom : parent.bottom
}
color: Style.title.background
}
TLSCertPinIssueBar {
id: tlsBar
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
leftMargin: Style.main.border
rightMargin: Style.main.border
}
enabled : root.activeContent
}
InformationBar {
id: infoBar
anchors {
left : parent.left
right : parent.right
top : tlsBar.bottom
leftMargin: Style.main.border
rightMargin: Style.main.border
}
enabled : root.activeContent
}
TabLabels {
id: tabbar
currentIndex : 0
enabled: root.activeContent
anchors {
top : infoBar.bottom
right : parent.right
left : parent.left
leftMargin: Style.main.border
rightMargin: Style.main.border
}
model: [
{ "title" : qsTr("Accounts" , "title of tab that shows account list" ), "iconText": Style.fa.user_circle_o },
{ "title" : qsTr("Settings" , "title of tab that allows user to change settings" ), "iconText": Style.fa.cog },
{ "title" : qsTr("Help" , "title of tab that shows the help menu" ), "iconText": Style.fa.life_ring }
]
}
// Content of tabs
StackLayout {
id: viewContent
enabled: root.activeContent
// dimensions
anchors {
left : parent.left
right : parent.right
top : tabbar.bottom
bottom : parent.bottom
leftMargin: Style.main.border
rightMargin: Style.main.border
bottomMargin: Style.main.border
}
// attributes
currentIndex : { return root.tabbar.currentIndex}
clip : true
// content
AccountView {
id: viewAccount
onAddAccount: dialogAddUser.show()
model: accountsModel
delegate: AccountDelegate {
row_width: viewContent.width
}
}
SettingsView { id: viewSettings; }
HelpView { id: viewHelp; }
}
// Floating things
// Triangle
Rectangle {
id: tabtriangle
visible: false
property int margin : Style.main.leftMargin+ Style.tabbar.widthButton/2
anchors {
top : tabbar.bottom
left : tabbar.left
leftMargin : tabtriangle.margin - tabtriangle.width/2 + tabbar.currentIndex * tabbar.spacing
}
width: 2*Style.tabbar.heightTriangle
height: Style.tabbar.heightTriangle
color: "transparent"
Canvas {
anchors.fill: parent
onPaint: {
var ctx = getContext("2d")
ctx.fillStyle = Style.tabbar.background
ctx.moveTo(0 , 0)
ctx.lineTo(width/2, height)
ctx.lineTo(width , 0)
ctx.closePath()
ctx.fill()
}
}
}
// Bubble prevent action
Rectangle {
anchors {
left: parent.left
right: parent.right
top: titleBar.bottom
bottom: parent.bottom
}
visible: bubbleNote.visible
color: "#aa222222"
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
}
BubbleNote {
id : bubbleNote
visible : false
Component.onCompleted : {
bubbleNote.place(0)
}
}
BubbleNote {
id:addAccountTip
anchors.topMargin: viewAccount.separatorNoAccount - 2*Style.main.fontSize
text : qsTr("Click here to start", "on first launch, this is displayed above the Add Account button to tell the user what to do first")
state: (go.isFirstStart && viewAccount.numAccounts==0 && root.viewContent.currentIndex==0) ? "Visible" : "Invisible"
bubbleColor: Style.main.textBlue
Component.onCompleted : {
addAccountTip.place(-1)
}
enabled: false
states: [
State {
name: "Visible"
// hack: opacity 100% makes buttons dialog windows quit wrong color
PropertyChanges{target: addAccountTip; opacity: 0.999; visible: true}
},
State {
name: "Invisible"
PropertyChanges{target: addAccountTip; opacity: 0.0; visible: false}
}
]
transitions: [
Transition {
from: "Visible"
to: "Invisible"
SequentialAnimation{
NumberAnimation {
target: addAccountTip
property: "opacity"
duration: 0
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: addAccountTip
property: "visible"
duration: 0
}
}
},
Transition {
from: "Invisible"
to: "Visible"
SequentialAnimation{
NumberAnimation {
target: addAccountTip
property: "visible"
duration: 300
}
NumberAnimation {
target: addAccountTip
property: "opacity"
duration: 500
easing.type: Easing.InOutQuad
}
}
}
]
}
// Dialogs
DialogFirstStart {
id: dialogFirstStart
visible: go.isFirstStart && gui.isFirstWindow && !dialogGlobal.visible
}
// Dialogs
DialogPortChange {
id: dialogChangePort
}
DialogConnectionTroubleshoot {
id: dialogConnectionTroubleshoot
}
DialogAddUser {
id: dialogAddUser
onCreateAccount: Qt.openUrlExternally("https://protonmail.com/signup")
}
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 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)
}
}
}
}
Dialog {
id: dialogCredits
title: qsTr("Credits", "link to click on to view list of credited libraries")
Credits { }
}
DialogTLSCertInfo {
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 : ""
answer : ""
z: 100
}
// resize
MouseArea {
property int diff: 0
anchors {
bottom : parent.bottom
left : parent.left
right : parent.right
}
cursorShape: Qt.SizeVerCursor
height: Style.main.fontSize
onPressed: {
var globPos = mapToGlobal(mouse.x, mouse.y)
diff = root.height
diff -= globPos.y
}
onMouseYChanged : {
var globPos = mapToGlobal(mouse.x, mouse.y)
root.height = Math.max(root.minimumHeight, globPos.y + diff)
}
}
function showAndRise(){
go.loadAccounts()
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
// Toggle window
function toggle() {
go.loadAccounts()
if (root.visible) {
if (!root.active) {
root.raise()
root.requestActivate()
} else {
root.hide()
}
} else {
root.show()
root.raise()
}
}
onClosing: {
close.accepted = false
// NOTE: In order to make an initial accounts load
root.hide()
gui.closeMainWindow()
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2020 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/>.

View File

@ -0,0 +1,148 @@
// Copyright (c) 2020 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/>.
// Popup
import QtQuick 2.8
import QtQuick.Window 2.2
import BridgeUI 1.0
import ProtonUI 1.0
Window {
id:root
width : Style.info.width
height : Style.info.width/1.5
minimumWidth : Style.info.width
minimumHeight : Style.info.width/1.5
maximumWidth : Style.info.width
maximumHeight : Style.info.width/1.5
color : Style.main.background
flags : Qt.Window | Qt.Popup | Qt.FramelessWindowHint
visible : false
title : ""
x: 10
y: 10
property string messageID: ""
// Drag and move
MouseArea {
property point diff: "0,0"
property QtObject window: root
anchors {
fill: parent
}
onPressed: {
diff = Qt.point(window.x, window.y)
var mousePos = mapToGlobal(mouse.x, mouse.y)
diff.x -= mousePos.x
diff.y -= mousePos.y
}
onPositionChanged: {
var currPos = mapToGlobal(mouse.x, mouse.y)
window.x = currPos.x + diff.x
window.y = currPos.y + diff.y
}
}
Column {
topPadding: Style.main.fontSize
spacing: (root.height - (description.height + cancel.height + countDown.height + Style.main.fontSize))/3
width: root.width
Text {
id: description
color : Style.main.text
font.pointSize : Style.main.fontSize*Style.pt/1.2
anchors.horizontalCenter : parent.horizontalCenter
horizontalAlignment : Text.AlignHCenter
width : root.width - 2*Style.main.leftMargin
wrapMode : Text.Wrap
textFormat : Text.RichText
text: qsTr("The message with subject %1 has one or more recipients with no encryption settings. If you do not want to send this email click the cancel button.").arg("<h3>"+root.title+"</h3>")
}
Row {
spacing : Style.dialog.spacing
anchors.horizontalCenter: parent.horizontalCenter
ButtonRounded {
id: cancel
onClicked : root.hide(true)
height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3
fa_icon: Style.fa.send
text: qsTr("Send now", "Confirmation of sending unencrypted email.")
}
ButtonRounded {
id: sendAnyway
onClicked : root.hide(false)
height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3
fa_icon: Style.fa.times
text: qsTr("Cancel", "Cancel the sending of current email")
}
}
Text {
id: countDown
color: Style.main.text
font.pointSize : Style.main.fontSize*Style.pt/1.2
anchors.horizontalCenter : parent.horizontalCenter
horizontalAlignment : Text.AlignHCenter
width : root.width - 2*Style.main.leftMargin
wrapMode : Text.Wrap
textFormat : Text.RichText
text: qsTr("This popup will close after %1 and email will be sent unless you click the cancel button.").arg( "<b>" + timer.secLeft + "s</b>")
}
}
Timer {
id: timer
property var secLeft: 0
interval: 1000 //ms
repeat: true
onTriggered: {
secLeft--
if (secLeft <= 0) {
root.hide(true)
}
}
}
function hide(shouldSend) {
root.visible = false
timer.stop()
go.saveOutgoingNoEncPopupCoord(root.x, root.y)
go.shouldSendAnswer(root.messageID, shouldSend)
}
function show(messageID, subject) {
root.messageID = messageID
root.title = subject
root.visible = true
timer.secLeft = 10
timer.start()
}
}

View File

@ -0,0 +1,180 @@
// Copyright (c) 2020 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/>.
// List the settings
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
import QtQuick.Controls 2.4
Item {
id: root
// must have wrapper
ScrollView {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: true
background: Rectangle {
color: Style.main.background
}
// content
Column {
anchors.left : parent.left
ButtonIconText {
id: cacheClear
text: qsTr("Clear Cache", "button to clear cache in settings")
leftIcon.text : Style.fa.times
rightIcon {
text : qsTr("Clear", "clickable link next to clear cache button in settings")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogGlobal.state="clearCache"
dialogGlobal.show()
}
}
ButtonIconText {
id: cacheKeychain
text: qsTr("Clear Keychain", "button to clear keychain in settings")
leftIcon.text : Style.fa.chain_broken
rightIcon {
text : qsTr("Clear", "clickable link next to clear keychain button in settings")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogGlobal.state="clearChain"
dialogGlobal.show()
}
}
ButtonIconText {
id: autoStart
text: qsTr("Automatically start Bridge", "label for toggle that activates and disables the automatic start")
leftIcon.text : Style.fa.rocket
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : go.isAutoStart!=false ? Style.fa.toggle_on : Style.fa.toggle_off
color : go.isAutoStart!=false ? Style.main.textBlue : Style.main.textDisabled
}
Accessible.description: (
go.isAutoStart == false ?
qsTr("Enable" , "Click to enable the automatic start of Bridge") :
qsTr("Disable" , "Click to disable the automatic start of Bridge")
) + " " + text
onClicked: {
go.toggleAutoStart()
}
}
ButtonIconText {
id: advancedSettings
property bool isAdvanced : !go.isDefaultPort
text: qsTr("Advanced settings", "button to open the advanced settings list in the settings page")
leftIcon.text : Style.fa.cogs
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : isAdvanced!=0 ? Style.fa.chevron_circle_up : Style.fa.chevron_circle_right
color : isAdvanced!=0 ? Style.main.textDisabled : Style.main.textBlue
}
Accessible.description: (
isAdvanced ?
qsTr("Hide", "Click to hide the advance settings") :
qsTr("Show", "Click to show the advance settings")
) + " " + text
onClicked: {
isAdvanced = !isAdvanced
}
}
ButtonIconText {
id: changePort
visible: advancedSettings.isAdvanced
text: qsTr("Change IMAP & SMTP settings", "button to change IMAP and SMTP ports in settings")
leftIcon.text : Style.fa.plug
rightIcon {
text : qsTr("Change", "clickable link next to change ports button in settings")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogChangePort.show()
}
}
ButtonIconText {
id: reportNoEnc
text: qsTr("Notification of outgoing email without encryption", "Button to set whether to report or send an email without encryption")
visible: advancedSettings.isAdvanced
leftIcon.text : Style.fa.ban
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : go.isReportingOutgoingNoEnc ? Style.fa.toggle_on : Style.fa.toggle_off
color : go.isReportingOutgoingNoEnc ? Style.main.textBlue : Style.main.textDisabled
}
Accessible.description: (
go.isReportingOutgoingNoEnc == 0 ?
qsTr("Enable" , "Click to report an email without encryption") :
qsTr("Disable" , "Click to send without asking an email without encryption")
) + " " + text
onClicked: {
go.toggleIsReportingOutgoingNoEnc()
}
}
ButtonIconText {
id: allowProxy
visible: advancedSettings.isAdvanced
text: qsTr("Allow alternative routing", "label for toggle that allows and disallows using a proxy")
leftIcon.text : Style.fa.rocket
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : go.isProxyAllowed!=false ? Style.fa.toggle_on : Style.fa.toggle_off
color : go.isProxyAllowed!=false ? Style.main.textBlue : Style.main.textDisabled
}
Accessible.description: (
go.isProxyAllowed == false ?
qsTr("Enable" , "Click to allow alternative routing") :
qsTr("Disable" , "Click to disallow alternative routing")
) + " " + text
onClicked: {
dialogGlobal.state="toggleAllowProxy"
dialogGlobal.show()
}
}
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2020 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/>.

View File

@ -0,0 +1,127 @@
// Copyright (c) 2020 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
}
}
}
}
}

View File

@ -0,0 +1,15 @@
module BridgeUI
AccountDelegate 1.0 AccountDelegate.qml
Credits 1.0 Credits.qml
DialogFirstStart 1.0 DialogFirstStart.qml
DialogPortChange 1.0 DialogPortChange.qml
DialogYesNo 1.0 DialogYesNo.qml
DialogTLSCertInfo 1.0 DialogTLSCertInfo.qml
HelpView 1.0 HelpView.qml
InfoWindow 1.0 InfoWindow.qml
MainWindow 1.0 MainWindow.qml
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

View File

@ -0,0 +1,314 @@
// Copyright (c) 2020 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/>.
// This is main qml file
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
// All imports from dynamic must be loaded before
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
Item {
id: gui
property MainWindow winMain
property bool isFirstWindow: true
property int warningFlags: 0
InfoWindow { id: infoWin }
OutgoingNoEncPopup { id: outgoingNoEncPopup }
BugReportWindow {
id: bugreportWin
clientVersion.visible : true
// pre-fill the form
onPrefill : {
userAddress.text=""
if (accountsModel.count>0) {
var addressList = accountsModel.get(0).aliases.split(";")
if (addressList.length>0) {
userAddress.text = addressList[0]
}
}
clientVersion.text=go.getLastMailClient()
}
}
onWarningFlagsChanged : {
if (gui.warningFlags==Style.okInfoBar) {
go.normalSystray()
} else {
if ((gui.warningFlags & Style.errorInfoBar) == Style.errorInfoBar) {
go.errorSystray()
} else {
go.highlightSystray()
}
}
}
// Signals from Go
Connections {
target: go
onShowWindow : {
gui.openMainWindow()
}
onShowHelp : {
gui.openMainWindow(false)
winMain.tabbar.currentIndex = 2
winMain.showAndRise()
}
onShowQuit : {
gui.openMainWindow(false)
winMain.dialogGlobal.state="quit"
winMain.dialogGlobal.show()
winMain.showAndRise()
}
onProcessFinished : {
winMain.dialogGlobal.hide()
winMain.dialogAddUser.hide()
winMain.dialogChangePort.hide()
infoWin.hide()
}
onOpenManual : Qt.openUrlExternally("http://protonmail.com/bridge")
onNotifyBubble : {
gui.showBubble(tabIndex, message, true)
}
onSilentBubble : {
gui.showBubble(tabIndex, message, false)
}
onBubbleClosed : {
gui.warningFlags &= ~Style.warnBubbleMessage
}
onSetConnectionStatus: {
go.isConnectionOK = isAvailable
gui.openMainWindow(false)
if (go.isConnectionOK) {
if( winMain.updateState=="noInternet") {
go.setUpdateState("upToDate")
}
} else {
go.setUpdateState("noInternet")
}
}
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") {
winMain.updateState = updateState
}
}
onSetAddAccountWarning : winMain.dialogAddUser.setWarning(message, 0)
onNotifyVersionIsTheLatest : {
go.silentBubble(2,qsTr("You have the latest version!", "notification", -1))
}
onNotifyUpdate : {
go.setUpdateState("forceUpdate")
if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
go.runCheckVersion(false)
winMain.dialogUpdate.show()
}
}
onNotifyLogout : {
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Bridge with this account.").arg(accname) )
}
onNotifyAddressChanged : {
go.notifyBubble(0, qsTr("The address list has been changed for account %1. You may need to reconfigure the settings in your email client.").arg(accname) )
}
onNotifyAddressChangedLogout : {
go.notifyBubble(0, qsTr("The address list has been changed for account %1. You have to reconfigure the settings in your email client.").arg(accname) )
}
onNotifyPortIssue : { // busyPortIMAP , busyPortSMTP
if (!busyPortIMAP && !busyPortSMTP) { // at least one must have issues to show warning
return
}
gui.openMainWindow(false)
winMain.tabbar.currentIndex=1
go.isDefaultPort = false
var text
if (busyPortIMAP && busyPortSMTP) { // both have problems
text = qsTr("The default ports used by Bridge for IMAP (%1) and SMTP (%2) are occupied by one or more other applications." , "the first part of notification text (two ports)").arg(go.getIMAPPort()).arg(go.getSMTPPort())
text += " "
text += qsTr("To change the ports for these servers, go to Settings -> Advanced Settings.", "the second part of notification text (two ports)")
} else { // only one is occupied
var server, port
if (busyPortSMTP) {
server = "SMTP"
port = go.getSMTPPort()
} else {
server = "IMAP"
port = go.getIMAPPort()
}
text = qsTr("The default port used by Bridge for %1 (%2) is occupied by another application.", "the first part of notification text (one port)").arg(server).arg(port)
text += " "
text += qsTr("To change the port for this server, go to Settings -> Advanced Settings.", "the second part of notification text (one port)")
}
go.notifyBubble(1, text )
}
onNotifyKeychainRebuild : {
go.notifyBubble(1, qsTr(
"Your MacOS keychain is probably corrupted. Please consult the instructions in our <a href=\"https://protonmail.com/bridge/faq#c15\">FAQ</a>.",
"notification message"
))
}
onNotifyHasNoKeychain : {
gui.winMain.dialogGlobal.state="noKeychain"
gui.winMain.dialogGlobal.show()
}
onShowNoActiveKeyForRecipient : {
go.notifyBubble(0, qsTr(
"Key pinning is enabled for %1 but no active key is pinned. " +
"You must pin the key in order to send a message to this address. " +
"You can find instructions " +
"<a href=\"https://protonmail.com/support/knowledge-base/key-pinning/\">here</a>."
).arg(recipient))
}
onFailedAutostartCode : {
gui.openMainWindow(true)
switch (code) {
case "permission" : // linux+darwin
case "85070005" : // windows
go.notifyBubble(1, go.failedAutostartPerm)
break
case "81004003" : // windows
go.notifyBubble(1, go.failedAutostart+" "+qsTr("Can not create instance.", "for autostart"))
break
case "" :
default :
go.notifyBubble(1, go.failedAutostart)
}
}
onShowOutgoingNoEncPopup : {
outgoingNoEncPopup.show(messageID, subject)
}
onSetOutgoingNoEncPopupCoord : {
outgoingNoEncPopup.x = x
outgoingNoEncPopup.y = y
}
onUpdateFinished : {
winMain.dialogUpdate.finished(hasError)
}
onShowCertIssue : {
winMain.tlsBarState="notOK"
}
}
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){
if (Style.fontawesome.status == FontLoader.Loading) continue
if (Style.fontawesome.status != FontLoader.Ready) console.log("Error while loading font")
break
}
if (typeof(showAndRise)==='undefined') {
showAndRise = true
}
if (gui.winMain == null) {
gui.winMain = Qt.createQmlObject(
'import BridgeUI 1.0; MainWindow {visible : false}',
gui, "winMain"
)
}
if (showAndRise) {
gui.winMain.showAndRise()
}
}
function closeMainWindow () {
gui.winMain.hide()
gui.winMain.destroy(5000)
gui.winMain = null
gui.isFirstWindow = false
}
function showBubble(tabIndex, message, isWarning) {
gui.openMainWindow(true)
if (isWarning) {
gui.warningFlags |= Style.warnBubbleMessage
}
winMain.bubbleNote.text = message
winMain.bubbleNote.place(tabIndex)
winMain.bubbleNote.show()
}
// On start
Component.onCompleted : {
// set messages for translations
go.wrongCredentials = qsTr("Incorrect username or password." , "notification", -1)
go.wrongMailboxPassword = qsTr("Incorrect mailbox password." , "notification", -1)
go.canNotReachAPI = qsTr("Cannot contact server, please check your internet connection." , "notification", -1)
go.versionCheckFailed = qsTr("Version check was unsuccessful. Please try again later." , "notification", -1)
go.credentialsNotRemoved = qsTr("Credentials could not be removed." , "notification", -1)
go.failedAutostartPerm = qsTr("Unable to configure automatic start due to permissions settings - see <a href=\"https://protonmail.com/bridge/faq#c11\">FAQ</a> for details.", "notification", -1)
go.failedAutostart = qsTr("Unable to configure automatic start." , "notification", -1)
go.genericErrSeeLogs = qsTr("An error happened during procedure. See logs for more details." , "notification", -1)
// 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()
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) 2020 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/>.
// default options to make button accessible
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
Button {
function clearText(value) {
// remove font-awesome chars
return value.replace(/[\uf000-\uf2e0]/g,'')
}
Accessible.onPressAction: clicked()
Accessible.ignored: !enabled || !visible
Accessible.name: clearText(text)
Accessible.description: clearText(text)
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020 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/>.
// default options to make text accessible and selectable
import QtQuick 2.8
import ProtonUI 1.0
TextEdit {
function clearText(value) {
// substitue the copyright symbol by the text and remove the font-awesome chars and HTML tags
return value.replace(/\uf1f9/g,'Copyright').replace(/[\uf000-\uf2e0]/g,'').replace(/<[^>]+>/g,'')
}
readOnly: true
selectByKeyboard: true
selectByMouse: true
Accessible.role: Accessible.StaticText
Accessible.name: clearText(text)
Accessible.description: clearText(text)
Accessible.focusable: true
Accessible.ignored: !enabled || !visible || text == ""
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020 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/>.
// default options to make text accessible
import QtQuick 2.8
import ProtonUI 1.0
Text {
function clearText(value) {
// substitue the copyright symbol by the text and remove the font-awesome chars and HTML tags
return value.replace(/\uf1f9/g,'Copyright').replace(/[\uf000-\uf2e0]/g,'').replace(/<[^>]+>/g,'')
}
Accessible.role: Accessible.StaticText
Accessible.name: clearText(text)
Accessible.description: clearText(text)
Accessible.focusable: true
Accessible.ignored: !enabled || !visible || text == ""
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}
}

View File

@ -0,0 +1,140 @@
// Copyright (c) 2020 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/>.
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
Item {
id: root
signal addAccount()
property alias numAccounts : listAccounts.count
property alias model : listAccounts.model
property alias delegate : listAccounts.delegate
property int separatorNoAccount : viewContent.height-Style.accounts.heightFooter
property bool hasFooter : true
// must have wrapper
Rectangle {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
color: Style.main.background
// content
ListView {
id: listAccounts
anchors {
top : parent.top
left : parent.left
right : parent.right
bottom : hasFooter ? addAccFooter.top : parent.bottom
}
orientation: ListView.Vertical
clip: true
cacheBuffer: 2500
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
anchors {
right: parent.right
rightMargin: Style.main.rightMargin/4
}
width: Style.main.rightMargin/3
Accessible.ignored: true
}
header: Rectangle {
width : wrapper.width
height : root.numAccounts!=0 ? Style.accounts.heightHeader : root.separatorNoAccount
color : "transparent"
AccessibleText { // Placeholder on empty
anchors {
centerIn: parent
}
visible: root.numAccounts==0
text : qsTr("No accounts added", "displayed when there are no accounts added")
font.pointSize : Style.main.fontSize * Style.pt
color : Style.main.textDisabled
}
Text { // Account
anchors {
left : parent.left
leftMargin : Style.main.leftMargin
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0
font.bold : true
font.pointSize : Style.main.fontSize * Style.pt
text : qsTr("ACCOUNT", "title of column that displays account name")
color : Style.main.textDisabled
}
Text { // Status
anchors {
left : parent.left
leftMargin : Style.accounts.leftMargin2
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0
font.bold : true
font.pointSize : Style.main.fontSize * Style.pt
text : qsTr("STATUS", "title of column that displays connected or disconnected status")
color : Style.main.textDisabled
}
Text { // Actions
anchors {
left : parent.left
leftMargin : Style.accounts.leftMargin3
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0
font.bold : true
font.pointSize : Style.main.fontSize * Style.pt
text : qsTr("ACTIONS", "title of column that displays log out and log in actions for each account")
color : Style.main.textDisabled
}
// line
Rectangle {
anchors {
left : parent.left
right : parent.right
bottom : parent.bottom
}
visible: root.numAccounts!=0
color: Style.accounts.line
height: Style.accounts.heightLine
}
}
}
AddAccountBar {
id: addAccFooter
visible: hasFooter
anchors {
left : parent.left
bottom : parent.bottom
}
}
}
Shortcut {
sequence: StandardKey.SelectAll
onActivated: root.addAccount()
}
}

View File

@ -0,0 +1,69 @@
// Copyright (c) 2020 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/>.
// Bar with add account button and help
import QtQuick 2.8
import ProtonUI 1.0
Rectangle {
width : parent.width
height : Style.accounts.heightFooter
color: "transparent"
Rectangle {
anchors {
top : parent.top
left : parent.left
right : parent.right
}
height: Style.accounts.heightLine
color: Style.accounts.line
}
ClickIconText {
id: buttonAddAccount
anchors {
left : parent.left
leftMargin : Style.main.leftMargin
verticalCenter : parent.verticalCenter
}
textColor : Style.main.textBlue
iconText : Style.fa.plus_circle
text : qsTr("Add Account", "begins the flow to log in to an account that is not yet listed")
textBold : true
onClicked : root.addAccount()
Accessible.description: {
if (gui.winMain!=null) {
return text + (gui.winMain.addAccountTip.visible? ", "+gui.winMain.addAccountTip.text : "")
}
return buttonAddAccount.text
}
}
ClickIconText {
id: buttonHelp
anchors {
right : parent.right
rightMargin : Style.main.rightMargin
verticalCenter : parent.verticalCenter
}
textColor : Style.main.textDisabled
iconText : Style.fa.question_circle
text : qsTr("Help", "directs the user to the online user guide")
textBold : true
onClicked : go.openManual()
}
}

View File

@ -0,0 +1,170 @@
// Copyright (c) 2020 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/>.
// Notify user
import QtQuick 2.8
import ProtonUI 1.0
Rectangle {
id: root
property int posx // x-coordinate of triangle
property bool isTriangleBelow
property string text
property alias bubbleColor: bubble.color
anchors {
top : tabbar.bottom
left : tabbar.left
leftMargin : {
// position of bubble calculated from posx
return Math.max(
Style.main.leftMargin, // keep minimal left margin
Math.min(
root.posx - root.width/2, // fit triangle in the middle if possible
tabbar.width - root.width - Style.main.rightMargin // keep minimal right margin
)
)
}
topMargin: 0
}
height : triangle.height + bubble.height
width : bubble.width
color : "transparent"
visible : false
Rectangle {
id : triangle
anchors {
top : root.isTriangleBelow ? undefined : root.top
bottom : root.isTriangleBelow ? root.bottom : undefined
bottomMargin : 1*Style.px
left : root.left
leftMargin : root.posx - triangle.width/2 - root.anchors.leftMargin
}
width: 2*Style.tabbar.heightTriangle+2
height: Style.tabbar.heightTriangle
color: "transparent"
Canvas {
anchors.fill: parent
rotation: root.isTriangleBelow ? 180 : 0
onPaint: {
var ctx = getContext("2d")
ctx.fillStyle = bubble.color
ctx.moveTo(0 , height)
ctx.lineTo(width/2, 0)
ctx.lineTo(width , height)
ctx.closePath()
ctx.fill()
}
}
}
Rectangle {
id: bubble
anchors {
top: root.top
left: root.left
topMargin: (root.isTriangleBelow ? 0 : triangle.height)
}
width : mainText.contentWidth + Style.main.leftMargin + Style.main.rightMargin
height : 2*Style.main.fontSize
radius : Style.bubble.radius
color : Style.bubble.background
AccessibleText {
id: mainText
anchors {
horizontalCenter : parent.horizontalCenter
top: parent.top
topMargin : Style.main.fontSize
}
text: "<html><style>a { color: "+Style.main.textBlue+";}</style>"+root.text+"<html>"
width : Style.bubble.width - ( Style.main.leftMargin + Style.main.rightMargin )
font.pointSize: Style.main.fontSize * Style.pt
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
wrapMode: Text.WordWrap
color: Style.bubble.text
onLinkActivated: {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: mainText
acceptedButtons: Qt.NoButton
}
Accessible.name: qsTr("Message")
Accessible.description: root.text
}
ButtonRounded {
id: okButton
visible: !root.isTriangleBelow
anchors {
bottom : parent.bottom
horizontalCenter : parent.horizontalCenter
bottomMargin : Style.main.fontSize
}
text: qsTr("Okay", "confirms and dismisses a notification")
height: Style.main.fontSize*2
color_main: Style.main.text
color_minor: Style.main.textBlue
isOpaque: true
onClicked: hide()
}
}
function place(index) {
if (index < 0) {
// add accounts
root.isTriangleBelow = true
bubble.height = 3.25*Style.main.fontSize
root.posx = 2*Style.main.leftMargin
bubble.width = mainText.contentWidth - Style.main.leftMargin
} else {
root.isTriangleBelow = false
bubble.height = (
bubble.anchors.topMargin + // from top
mainText.contentHeight + // the text content
Style.main.fontSize + // gap between button
okButton.height + okButton.anchors.bottomMargin // from bottom and button
)
if (index < 3) {
// possition accordig to top tab
var margin = Style.main.leftMargin + Style.tabbar.widthButton/2
root.posx = margin + index*tabbar.spacing
} else {
// quit button
root.posx = tabbar.width - 2*Style.main.rightMargin
}
}
}
function show() {
root.visible=true
gui.winMain.activeContent = false
}
function hide() {
root.visible=false
go.bubbleClosed()
gui.winMain.activeContent = true
gui.winMain.tabbar.focusButton()
}
}

View File

@ -0,0 +1,337 @@
// Copyright (c) 2020 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/>.
// Window for sending a bug report
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import ProtonUI 1.0
import QtGraphicalEffects 1.0
Window {
id:root
property alias userAddress : userAddress
property alias clientVersion : clientVersion
width : Style.bugreport.width
height : Style.bugreport.height
minimumWidth : Style.bugreport.width
maximumWidth : Style.bugreport.width
minimumHeight : Style.bugreport.height
maximumHeight : Style.bugreport.height
property color inputBorderColor : Style.main.text
color : "transparent"
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
title : "ProtonMail Bridge - Bug report"
visible : false
WindowTitleBar {
id: titleBar
window: root
}
Rectangle {
id:background
color: Style.main.background
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
bottom : parent.bottom
}
border {
width: Style.main.border
color: Style.tabbar.background
}
}
Rectangle {
id:content
anchors {
fill : parent
leftMargin : Style.main.leftMargin
rightMargin : Style.main.rightMargin
bottomMargin : Style.main.rightMargin
topMargin : Style.main.rightMargin + titleBar.height
}
color: "transparent"
// Description in flickable
Flickable {
id: descripWrapper
anchors {
left: parent.left
right: parent.right
top: parent.top
}
height: content.height - (
(clientVersion.visible ? clientVersion.height + Style.dialog.fontSize : 0) +
userAddress.height + Style.dialog.fontSize +
securityNote.contentHeight + Style.dialog.fontSize +
cancelButton.height + Style.dialog.fontSize
)
clip: true
contentWidth : width
contentHeight : height
TextArea.flickable: TextArea {
id: description
focus: true
wrapMode: TextEdit.Wrap
placeholderText: qsTr ("Please briefly describe the bug(s) you have encountered...", "bug report instructions")
background : Rectangle {
color : Style.dialog.background
radius: Style.dialog.radiusButton
border {
color : root.inputBorderColor
width : Style.dialog.borderInput
}
layer.enabled: true
layer.effect: FastBlur {
anchors.fill: parent
radius: 8 * Style.px
}
}
color: Style.main.text
font.pointSize: Style.dialog.fontSize * Style.pt
selectionColor: Style.main.textBlue
selectByKeyboard: true
selectByMouse: true
KeyNavigation.tab: clientVersion
KeyNavigation.priority: KeyNavigation.BeforeItem
}
ScrollBar.vertical : ScrollBar{}
}
// Client
TextLabel {
anchors {
left: parent.left
top: descripWrapper.bottom
topMargin: Style.dialog.fontSize
}
visible: clientVersion.visible
width: parent.width/2.618
text: qsTr ("Email client:", "in the bug report form, which third-party email client is being used")
font.pointSize: Style.dialog.fontSize * Style.pt
}
TextField {
id: clientVersion
anchors {
right: parent.right
top: descripWrapper.bottom
topMargin: Style.dialog.fontSize
}
placeholderText: qsTr("e.g. Thunderbird", "in the bug report form, placeholder text for email client")
width: parent.width/1.618
color : Style.dialog.text
selectionColor : Style.main.textBlue
selectByMouse : true
font.pointSize : Style.dialog.fontSize * Style.pt
padding : Style.dialog.radiusButton
background: Rectangle {
color : Style.dialog.background
radius: Style.dialog.radiusButton
border {
color : root.inputBorderColor
width : Style.dialog.borderInput
}
layer.enabled: true
layer.effect: FastBlur {
anchors.fill: parent
radius: 8 * Style.px
}
}
onAccepted: userAddress.focus = true
}
// Address
TextLabel {
anchors {
left: parent.left
top: clientVersion.visible ? clientVersion.bottom : descripWrapper.bottom
topMargin: Style.dialog.fontSize
}
color: Style.dialog.text
width: parent.width/2.618
text: qsTr ("Contact email:", "in the bug report form, an email to contact the user at")
font.pointSize: Style.dialog.fontSize * Style.pt
}
TextField {
id: userAddress
anchors {
right: parent.right
top: clientVersion.visible ? clientVersion.bottom : descripWrapper.bottom
topMargin: Style.dialog.fontSize
}
placeholderText: "benjerry@protonmail.com"
width: parent.width/1.618
color : Style.dialog.text
selectionColor : Style.main.textBlue
selectByMouse : true
font.pointSize : Style.dialog.fontSize * Style.pt
padding : Style.dialog.radiusButton
background: Rectangle {
color : Style.dialog.background
radius: Style.dialog.radiusButton
border {
color : root.inputBorderColor
width : Style.dialog.borderInput
}
layer.enabled: true
layer.effect: FastBlur {
anchors.fill: parent
radius: 8 * Style.px
}
}
onAccepted: root.submit()
}
// Note
AccessibleText {
id: securityNote
anchors {
left: parent.left
right: parent.right
top: userAddress.bottom
topMargin: Style.dialog.fontSize
}
wrapMode: Text.Wrap
color: Style.dialog.text
font.pointSize : Style.dialog.fontSize * Style.pt
text:
"<span style='font-family: " + Style.fontawesome.name + "'>" + Style.fa.exclamation_triangle + "</span> " +
qsTr("Bug reports are not end-to-end encrypted!", "The first part of warning in bug report form") + " " +
qsTr("Please do not send any sensitive information.", "The second part of warning in bug report form") + " " +
qsTr("Contact us at security@protonmail.com for critical security issues.", "The third part of warning in bug report form")
}
// buttons
ButtonRounded {
id: cancelButton
anchors {
left: parent.left
bottom: parent.bottom
}
fa_icon: Style.fa.times
text: qsTr ("Cancel", "dismisses current action")
onClicked : root.hide()
}
ButtonRounded {
anchors {
right: parent.right
bottom: parent.bottom
}
isOpaque: true
color_main: "white"
color_minor: Style.main.textBlue
fa_icon: Style.fa.send
text: qsTr ("Send", "button sends bug report")
onClicked : root.submit()
}
}
Rectangle {
id: notification
property bool isOK: true
visible: false
color: background.color
anchors.fill: background
Text {
anchors.centerIn: parent
color: Style.dialog.text
width: background.width*0.6180
text: notification.isOK ?
qsTr ( "Bug report successfully sent." , "notification message about bug sending" ) :
qsTr ( "Unable to submit bug report." , "notification message about bug sending" )
horizontalAlignment: Text.AlignHCenter
font.pointSize: Style.dialog.titleSize * Style.pt
}
Timer {
id: notificationTimer
interval: 3000
repeat: false
onTriggered : {
notification.visible=false
if (notification.isOK) root.hide()
}
}
}
function submit(){
if(root.areInputsOK()){
root.notify(go.sendBug(description.text, clientVersion.text, userAddress.text ))
}
}
function isEmpty(input){
if (input.text=="") {
input.focus=true
input.placeholderText = qsTr("Field required", "a field that must be filled in to submit form")
return true
}
return false
}
function areInputsOK() {
var isOK = true
if (isEmpty(userAddress)) { isOK=false }
if (clientVersion.visible && isEmpty(clientVersion)) { isOK=false }
if (isEmpty(description)) { isOK=false }
return isOK
}
function clear() {
description.text = ""
clientVersion.text = ""
notification.visible = false
}
signal prefill()
function notify(isOK){
notification.isOK = isOK
notification.visible = true
notificationTimer.start()
}
function show() {
prefill()
root.visible=true
}
function hide() {
clear()
root.visible=false
}
}

View File

@ -0,0 +1,100 @@
// Copyright (c) 2020 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/>.
// Button with full window width containing two icons (left and right) and text
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
AccessibleButton {
id: root
property alias leftIcon : leftIcon
property alias rightIcon : rightIcon
property alias main : mainText
// dimensions
width : viewContent.width
height : Style.main.heightRow
topPadding: 0
bottomPadding: 0
leftPadding: Style.main.leftMargin
rightPadding: Style.main.rightMargin
background : Rectangle{
color: Qt.lighter(Style.main.background, root.hovered || root.activeFocus ? ( root.pressed ? 1.2: 1.1) :1.0)
// line
Rectangle {
anchors.bottom : parent.bottom
width : parent.width
height : Style.main.heightLine
color : Style.main.line
}
// pointing cursor
MouseArea {
anchors.fill : parent
cursorShape : Qt.PointingHandCursor
acceptedButtons: Qt.NoButton
}
}
contentItem : Rectangle {
color: "transparent"
// Icon left
Text {
id: leftIcon
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
}
font {
family : Style.fontawesome.name
pointSize : Style.settings.iconSize * Style.pt
}
color : Style.main.textBlue
text : Style.fa.hashtag
}
// Icon/Text right
Text {
id: rightIcon
anchors {
verticalCenter : parent.verticalCenter
right : parent.right
}
font {
family : Style.fontawesome.name
pointSize : Style.settings.iconSize * Style.pt
}
color : Style.main.textBlue
text : Style.fa.hashtag
}
// Label
Text {
id: mainText
anchors {
verticalCenter : parent.verticalCenter
left : leftIcon.right
leftMargin : leftIcon.text!="" ? Style.main.leftMargin : 0
}
font.pointSize : Style.settings.fontSize * Style.pt
color : Style.main.text
text : root.text
}
}
}

View File

@ -0,0 +1,92 @@
// Copyright (c) 2020 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/>.
// Classic button with icon and text
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtGraphicalEffects 1.0
import ProtonUI 1.0
AccessibleButton {
id: root
property string fa_icon : ""
property color color_main : Style.dialog.text
property color color_minor : "transparent"
property bool isOpaque : false
text : "undef"
state : root.hovered || root.activeFocus ? "hover" : "normal"
width : Style.dialog.widthButton
height : Style.dialog.heightButton
scale : root.pressed ? 0.96 : 1.00
background: Rectangle {
border {
color : root.color_main
width : root.isOpaque ? 0 : Style.dialog.borderButton
}
radius : Style.dialog.radiusButton
color : root.isOpaque ? root.color_minor : "transparent"
MouseArea {
anchors.fill : parent
cursorShape : Qt.PointingHandCursor
acceptedButtons: Qt.NoButton
}
}
contentItem: Rectangle {
color: "transparent"
Row {
id: mainText
anchors.centerIn: parent
spacing: 0
Text {
font {
pointSize : Style.dialog.fontSize * Style.pt
family : Style.fontawesome.name
}
color : color_main
text : root.fa_icon=="" ? "" : root.fa_icon + " "
}
Text {
font {
pointSize : Style.dialog.fontSize * Style.pt
}
color : color_main
text : root.text
}
}
Glow {
id: mainTextEffect
anchors.fill : mainText
source: mainText
color: color_main
opacity: 0.33
}
}
states :[
State {name: "normal"; PropertyChanges{ target: mainTextEffect; radius: 0 ; visible: false } },
State {name: "hover" ; PropertyChanges{ target: mainTextEffect; radius: 3*Style.px ; visible: true } }
]
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2020 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/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
CheckBox {
id: root
spacing: Style.dialog.spacing
padding: 0
property color textColor : Style.main.text
property color checkedColor : Style.main.textBlue
property color uncheckedColor : Style.main.textInactive
property string checkedSymbol : Style.fa.check_square_o
property string uncheckedSymbol : Style.fa.square_o
background: Rectangle {
color: Style.transparent
}
indicator: Text {
text : root.checked ? root.checkedSymbol : root.uncheckedSymbol
color : root.checked ? root.checkedColor : root.uncheckedColor
font {
pointSize : Style.dialog.iconSize * Style.pt
family : Style.fontawesome.name
}
}
contentItem: Text {
id: label
text : root.text
color : root.textColor
font {
pointSize: Style.dialog.fontSize * Style.pt
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: Style.dialog.iconSize + root.spacing
}
}

View File

@ -0,0 +1,98 @@
// Copyright (c) 2020 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/>.
// No border button with icon
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
AccessibleButton {
id: root
property string iconText : Style.fa.hashtag
property color textColor : Style.main.text
property int fontSize : Style.main.fontSize
property int iconSize : Style.main.iconSize
property int margin : iconText!="" ? Style.main.leftMarginButton : 0.0
property bool iconOnRight : false
property bool textBold : false
property bool textUnderline : false
TextMetrics {
id: metrics
text: root.text
font: showText.font
}
TextMetrics {
id: metricsIcon
text : root.iconText
font : showIcon.font
}
scale : root.pressed ? 0.96 : root.activeFocus ? 1.05 : 1.0
height : Math.max(metrics.height, metricsIcon.height)
width : metricsIcon.width*1.5 + margin + metrics.width + 4.0
padding : 0.0
background : Rectangle {
color: Style.transparent
MouseArea {
anchors.fill : parent
cursorShape : Qt.PointingHandCursor
acceptedButtons: Qt.NoButton
}
}
contentItem : Rectangle {
color: Style.transparent
Text {
id: showIcon
anchors {
left : iconOnRight ? showText.right : parent.left
leftMargin : iconOnRight ? margin : 0
verticalCenter : parent.verticalCenter
}
font {
pointSize : iconSize * Style.pt
family : Style.fontawesome.name
}
color : textColor
text : root.iconText
}
Text {
id: showText
anchors {
verticalCenter : parent.verticalCenter
left : iconOnRight ? parent.left : showIcon.right
leftMargin : iconOnRight ? 0 : margin
}
color : textColor
font {
pointSize : root.fontSize * Style.pt
bold: root.textBold
underline: root.textUnderline
}
text : root.text
}
}
}

View File

@ -0,0 +1,147 @@
// Copyright (c) 2020 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/>.
// Dialog with adding new user
import QtQuick 2.8
import QtQuick.Layouts 1.3
import ProtonUI 1.0
StackLayout {
id: root
property string title : "title"
property string subtitle : ""
property alias timer : timer
property alias warning : warningText
property bool isDialogBusy : false
property real titleHeight : 2*titleText.anchors.topMargin + titleText.height + (warningText.visible ? warningText.anchors.topMargin + warningText.height : 0)
property Item background : Rectangle {
parent: root
width: root.width
height: root.height
color : Style.dialog.background
visible: root.visible
z: -1
AccessibleText {
id: titleText
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: Style.dialog.titleSize
}
font.pointSize : Style.dialog.titleSize * Style.pt
color : Style.dialog.text
text : root.title
}
AccessibleText {
id: subtitleText
anchors {
top: titleText.bottom
horizontalCenter: parent.horizontalCenter
}
font.pointSize : Style.dialog.fontSize * Style.pt
color : Style.dialog.text
text : root.subtitle
visible : root.subtitle != ""
}
AccessibleText {
id:warningText
anchors {
top: subtitleText.bottom
horizontalCenter: parent.horizontalCenter
}
font {
bold: true
pointSize: Style.dialog.fontSize * Style.pt
}
text : ""
color: Style.main.textBlue
visible: false
}
// prevent any action below
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
ClickIconText {
anchors {
top: parent.top
right: parent.right
topMargin: Style.dialog.titleSize
rightMargin: Style.dialog.titleSize
}
visible : !isDialogBusy
iconText : Style.fa.times
text : ""
onClicked : root.hide()
Accessible.description : qsTr("Close dialog %1", "Click to exit modal.").arg(root.title)
}
}
Accessible.role: Accessible.Grouping
Accessible.name: title
Accessible.description: title
Accessible.focusable: true
visible : false
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
bottom : parent.bottom
}
currentIndex : 0
signal show()
signal hide()
function incrementCurrentIndex() {
root.currentIndex++
}
function decrementCurrentIndex() {
root.currentIndex--
}
onShow: {
root.visible = true
root.forceActiveFocus()
}
onHide: {
root.timer.stop()
root.currentIndex=0
root.visible = false
root.timer.stop()
gui.winMain.tabbar.focusButton()
}
// QTimer is recommeded solution for creating trheads : http://doc.qt.io/qt-5/qtquick-threading-example.html
Timer {
id: timer
interval: 300 // wait for transistion
repeat: false
}
}

View File

@ -0,0 +1,464 @@
// Copyright (c) 2020 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/>.
// Dialog with adding new user
import QtQuick 2.8
import ProtonUI 1.0
Dialog {
id: root
title : ""
signal createAccount()
property alias inputPassword : inputPassword
property alias input2FAuth : input2FAuth
property alias inputPasswMailbox : inputPasswMailbox
//
property alias username : inputUsername.text
property alias usernameElided : usernameMetrics.elidedText
isDialogBusy : currentIndex==waitingAuthIndex || currentIndex==addingAccIndex
property bool isFirstAccount: false
property color buttonOpaqueMain : "white"
property int origin: 0
property int nameAndPasswordIndex : 0
property int waitingAuthIndex : 2
property int twoFAIndex : 1
property int mailboxIndex : 3
property int addingAccIndex : 4
property int newAccountIndex : 5
signal cancel()
signal okay()
TextMetrics {
id: usernameMetrics
font: dialogWaitingAuthText.font
elideWidth : Style.dialog.widthInput
elide : Qt.ElideMiddle
text : root.username
}
Column { // 0
id: dialogNameAndPassword
property int heightInputs : inputUsername.height + buttonRow.height + middleSep.height + inputPassword.height + middleSepPassw.height
Rectangle {
id: topSep
color : "transparent"
width : Style.main.dummy
height : root.height/2 - (dialogNameAndPassword.heightInputs)/2
}
InputField {
id: inputUsername
iconText : Style.fa.user_circle
label : qsTr("Username", "enter username to add account")
onAccepted : inputPassword.focusInput = true
}
Rectangle { id: middleSepPassw; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
InputField {
id: inputPassword
label : qsTr("Password", "password entry field")
iconText : Style.fa.lock
isPassword : true
onAccepted : root.okay()
}
Rectangle { id: middleSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.fontSize
ButtonRounded {
id:buttonCancel
fa_icon : Style.fa.times
text : qsTr("Cancel", "dismisses current action")
color_main : Style.dialog.text
onClicked : root.cancel()
}
ButtonRounded {
id: buttonNext
fa_icon : Style.fa.check
text : qsTr("Next", "navigate to next page in add account flow")
color_main : buttonOpaqueMain
color_minor : Style.dialog.textBlue
isOpaque : true
onClicked : root.okay()
}
}
Rectangle {
color : "transparent"
width : Style.main.dummy
height : root.height - (topSep.height + dialogNameAndPassword.heightInputs + Style.main.bottomMargin + signUpForAccount.height)
}
ClickIconText {
id: signUpForAccount
anchors.horizontalCenter: parent.horizontalCenter
fontSize : Style.dialog.fontSize
iconSize : Style.dialog.fontSize
iconText : "+"
text : qsTr ("Sign Up for an Account", "takes user to web page where they can create a ProtonMail account")
textBold : true
textUnderline : true
textColor : Style.dialog.text
onClicked : root.createAccount()
}
}
Column { // 1
id: dialog2FA
property int heightInputs : buttonRowPassw.height + middleSep2FA.height + input2FAuth.height
Rectangle {
color : "transparent"
width : Style.main.dummy
height : (root.height - dialog2FA.heightInputs)/2
}
InputField {
id: input2FAuth
label : qsTr("Two Factor Code", "two factor code entry field")
iconText : Style.fa.lock
onAccepted : root.okay()
}
Rectangle { id: middleSep2FA; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRowPassw
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.fontSize
ButtonRounded {
id: buttonBack
fa_icon: Style.fa.times
text: qsTr("Back", "navigate back in add account flow")
color_main: Style.dialog.text
onClicked : root.cancel()
}
ButtonRounded {
id: buttonNextTwo
fa_icon: Style.fa.check
text: qsTr("Next", "navigate to next page in add account flow")
color_main: buttonOpaqueMain
color_minor: Style.dialog.textBlue
isOpaque: true
onClicked : root.okay()
}
}
}
Column { // 2
id: dialogWaitingAuth
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogWaitingAuthText.height) /2 }
Text {
id: dialogWaitingAuthText
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font.pointSize: Style.dialog.fontSize * Style.pt
text : qsTr("Logging in") +"\n" + root.usernameElided
horizontalAlignment: Text.AlignHCenter
}
}
Column { // 3
id: dialogMailboxPassword
property int heightInputs : buttonRowMailbox.height + inputPasswMailbox.height + middleSepMailbox.height
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height - dialogMailboxPassword.heightInputs)/2}
InputField {
id: inputPasswMailbox
label : qsTr("Mailbox password for %1", "mailbox password entry field").arg(root.usernameElided)
iconText : Style.fa.lock
isPassword : true
onAccepted : root.okay()
}
Rectangle { id: middleSepMailbox; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRowMailbox
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.fontSize
ButtonRounded {
id: buttonBackBack
fa_icon: Style.fa.times
text: qsTr("Back", "navigate back in add account flow")
color_main: Style.dialog.text
onClicked : root.cancel()
}
ButtonRounded {
id: buttonLogin
fa_icon: Style.fa.check
text: qsTr("Next", "navigate to next page in add account flow")
color_main: buttonOpaqueMain
color_minor: Style.dialog.textBlue
isOpaque: true
onClicked : root.okay()
}
}
}
Column { // 4
id: dialogWaitingAccount
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height - dialogWaitingAccountText.height )/2 }
Text {
id: dialogWaitingAccountText
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
bold : true
pointSize: Style.dialog.fontSize * Style.pt
}
text : qsTr("Adding account, please wait ...", "displayed after user has logged in, before new account is displayed")
wrapMode: Text.Wrap
}
}
Column { // 5
id: dialogFirstUserAdded
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height - dialogWaitingAccountText.height - okButton.height*2 )/2 }
Text {
id: textFirstUser
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
bold : false
pointSize: Style.dialog.fontSize * Style.pt
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
text: "<html><style>a { font-weight: bold; text-decoration: none; color: white;}</style>"+
qsTr("Now you need to configure your client(s) to use the Bridge. Instructions for configuring your client can be found at", "") +
"<br/><a href=\"https://protonmail.com/bridge/clients\">https://protonmail.com/bridge/clients</a>.<html>"
wrapMode: Text.Wrap
onLinkActivated: {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink=="" ? Qt.PointingHandCursor : Qt.WaitCursor
acceptedButtons: Qt.NoButton
}
}
Rectangle { color : "transparent"; width : Style.main.dummy; height : okButton.height}
ButtonRounded{
id: okButton
anchors.horizontalCenter: parent.horizontalCenter
color_main: buttonOpaqueMain
color_minor: Style.main.textBlue
isOpaque: true
text: qsTr("Okay", "confirms and dismisses a notification")
onClicked: root.hide()
}
}
function clear_user() {
inputUsername.text = ""
inputUsername.rightIcon = ""
}
function clear_passwd() {
inputPassword.text = ""
inputPassword.rightIcon = ""
inputPassword.hidePasswordText()
}
function clear_2fa() {
input2FAuth.text = ""
input2FAuth.rightIcon = ""
}
function clear_passwd_mailbox() {
inputPasswMailbox.text = ""
inputPasswMailbox.rightIcon = ""
inputPasswMailbox.hidePasswordText()
}
onCancel : {
root.warning.visible=false
if (currentIndex==0) {
root.hide()
} else {
clear_passwd()
clear_passwd_mailbox()
currentIndex=0
}
}
function check_inputs() {
var isOK = true
switch (currentIndex) {
case nameAndPasswordIndex :
isOK &= inputUsername.checkNonEmpty()
isOK &= inputPassword.checkNonEmpty()
break
case twoFAIndex :
isOK &= input2FAuth.checkNonEmpty()
break
case mailboxIndex :
isOK &= inputPasswMailbox.checkNonEmpty()
break
}
if (isOK) {
warning.visible = false
warning.text= ""
} else {
setWarning(qsTr("Field required", "a field that must be filled in to submit form"),0)
}
return isOK
}
function setWarning(msg, changeIndex) {
// show message
root.warning.text = msg
root.warning.visible = true
}
onOkay : {
var isOK = check_inputs()
if (isOK) {
root.origin = root.currentIndex
switch (root.currentIndex) {
case nameAndPasswordIndex:
case twoFAIndex:
root.currentIndex = waitingAuthIndex
break;
case mailboxIndex:
root.currentIndex = addingAccIndex
}
timer.start()
}
}
onShow: {
root.title = qsTr ("Log in to your ProtonMail account", "displayed on screen when user enters username to begin adding account")
root.warning.visible = false
inputUsername.forceFocus()
root.isFirstAccount = go.isFirstStart && accountsModel.count==0
}
function startAgain() {
clear_passwd()
clear_2fa()
clear_passwd_mailbox()
root.currentIndex = nameAndPasswordIndex
root.inputPassword.focusInput = true
}
function finishLogin(){
root.currentIndex = addingAccIndex
var auth = go.addAccount(inputPasswMailbox.text)
if (auth<0) {
startAgain()
return
}
}
Connections {
target: timer
onTriggered : {
timer.repeat = false
switch (root.origin) {
case nameAndPasswordIndex:
var auth = go.login(inputUsername.text, inputPassword.text)
if (auth < 0) {
startAgain()
break
}
if (auth == 1) {
root.currentIndex = twoFAIndex
root.input2FAuth.focusInput = true
break
}
if (auth == 2) {
root.currentIndex = mailboxIndex
root.inputPasswMailbox.focusInput = true
break
}
root.inputPasswMailbox.text = inputPassword.text
root.finishLogin()
break;
case twoFAIndex:
var auth = go.auth2FA(input2FAuth.text)
if (auth < 0) {
startAgain()
break
}
if (auth == 1) {
root.currentIndex = mailboxIndex
root.inputPasswMailbox.focusInput = true
break
}
root.inputPasswMailbox.text = inputPassword.text
root.finishLogin()
break;
case mailboxIndex:
root.finishLogin()
break;
}
}
}
onHide: {
// because hide slot is conneceted to processFinished it will update
// the list evertyime `go` obejcet is finished
clear_passwd()
clear_passwd_mailbox()
clear_2fa()
clear_user()
go.loadAccounts()
if (root.isFirstAccount && accountsModel.count==1) {
root.isFirstAccount=false
root.currentIndex=5
root.show()
root.title=qsTr("Success, Account Added!", "shown after successful account addition")
}
}
Keys.onPressed: {
if (event.key == Qt.Key_Enter) {
root.okay()
}
}
}

View File

@ -0,0 +1,148 @@
// Copyright (c) 2020 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/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import ProtonUI 1.0
Dialog {
id: root
title : qsTr(
"Common connection problems and solutions",
"Title of the network troubleshooting modal"
)
isDialogBusy: false // can close
property var parContent : [
[
qsTr("Allow alternative routing" , "Paragraph title"),
qsTr(
"In case Proton sites are blocked, this setting allows Bridge "+
"to try alternative network routing to reach Proton, which can "+
"be useful for bypassing firewalls or network issues. We recommend "+
"keeping this setting on for greater reliability. "+
'<a href="https://protonmail.com/blog/anti-censorship-alternative-routing/">Learn more</a>'+
" and "+
'<a href="showProxy">enable here</a>'+
".",
"Paragraph content"
),
],
[
qsTr("No internet connection" , "Paragraph title"),
qsTr(
"Please make sure that your internet connection is working.",
"Paragraph content"
),
],
[
qsTr("Internet Service Provider (ISP) problem" , "Paragraph title"),
qsTr(
"Try connecting to Proton from a different network (or use "+
'<a href="https://protonvpn.com/">ProtonVPN</a>'+
" or "+
'<a href="https://torproject.org/">Tor</a>'+
").",
"Paragraph content"
),
],
[
qsTr("Government block" , "Paragraph title"),
qsTr(
"Your country may be blocking access to Proton. Try using "+
'<a href="https://protonvpn.com/">ProtonVPN</a>'+
" (or any other VPN) or "+
'<a href="https://torproject.org/">Tor</a>'+
".",
"Paragraph content"
),
],
[
qsTr("Antivirus interference" , "Paragraph title"),
qsTr(
"Temporarily disable or remove your antivirus software.",
"Paragraph content"
),
],
[
qsTr("Proxy/Firewall interference" , "Paragraph title"),
qsTr(
"Disable any proxies or firewalls, or contact your network administrator.",
"Paragraph content"
),
],
[
qsTr("Still cant find a solution" , "Paragraph title"),
qsTr(
"Contact us directly through our "+
'<a href="https://protonmail.com/support-form">support form</a>'+
", email (support@protonmail.com), or "+
'<a href="https://twitter.com/ProtonMail">Twitter</a>'+
".",
"Paragraph content"
),
],
[
qsTr("Proton is down" , "Paragraph title"),
qsTr(
"Check "+
'<a href="https://protonstatus.com/">Proton Status</a>'+
" for our system status.",
"Paragraph content"
),
],
]
Item {
AccessibleText {
anchors.centerIn: parent
color: Style.old.pm_white
linkColor: color
width: parent.width - 50 * Style.px
wrapMode: Text.WordWrap
font.pointSize: Style.main.fontSize*Style.pt
onLinkActivated: {
if (link=="showProxy") {
dialogGlobal.state= "toggleAllowProxy"
dialogGlobal.show()
} else {
Qt.openUrlExternally(link)
}
}
text: {
var content=""
for (var i=0; i<root.parContent.length; i++) {
var par = root.parContent[i]
content += "<p>"
content += "<b>"+par[0]+":</b> "
content += par[1]
content += "</p>\n"
}
return content
}
}
}
}

View File

@ -0,0 +1,250 @@
// Copyright (c) 2020 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/>.
// default options to make button accessible
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
Dialog {
id: root
title: "Bridge update "+go.newversion
property alias introductionText : introduction.text
property bool hasError : false
signal cancel()
signal okay()
isDialogBusy: currentIndex==1
Rectangle { // 0: Release notes and confirm
width: parent.width
height: parent.height
color: Style.transparent
Column {
anchors.centerIn: parent
spacing: 5*Style.dialog.spacing
AccessibleText {
id:introduction
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
linkColor: Style.dialog.textBlue
font {
pointSize: 0.8 * 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>'
onLinkActivated : {
console.log("clicked link:", link)
if (link == "releaseNotes"){
root.hide()
winMain.dialogVersionInfo.show()
} else {
root.hide()
Qt.openUrlExternally(link)
}
}
MouseArea {
anchors.fill: parent
cursorShape: introduction.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
ButtonRounded {
fa_icon: Style.fa.times
text: (go.goos=="linux" ? qsTr("Okay") : qsTr("Cancel"))
color_main: Style.dialog.text
onClicked: root.cancel()
}
ButtonRounded {
fa_icon: Style.fa.check
text: qsTr("Update")
visible: go.goos!="linux"
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
onClicked: root.okay()
}
}
}
}
Rectangle { // 0: Check / download / unpack / prepare
id: updateStatus
width: parent.width
height: parent.height
color: Style.transparent
Column {
anchors.centerIn: parent
spacing: Style.dialog.spacing
AccessibleText {
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: false
}
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 ""
}
}
}
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
background: Rectangle {
radius : Style.exporting.boxRadius
color : Style.exporting.progressBackground
}
contentItem: Item {
Rectangle {
id: finishedPartBar
width : parent.width * progressbar.visualPosition
height : parent.height
radius : Style.exporting.boxRadius
gradient : Gradient {
GradientStop { position: 0.00; color: Qt.lighter(Style.main.textBlue,1.1) }
GradientStop { position: 0.66; color: Style.main.textBlue }
GradientStop { position: 1.00; color: Qt.darker(Style.main.textBlue,1.1) }
}
Behavior on width {
NumberAnimation { duration:300; easing.type: Easing.InOutQuad }
}
}
Text {
anchors.centerIn: parent
text: ""
color: Style.main.background
font {
pointSize: Style.dialog.fontSize * Style.pt
}
}
}
}
}
}
Rectangle { // 1: Something went wrong / All ok, closing bridge
width: parent.width
height: parent.height
color: Style.transparent
Column {
anchors.centerIn: parent
spacing: 5*Style.dialog.spacing
AccessibleText {
color: Style.dialog.text
linkColor: Style.dialog.textBlue
font {
pointSize: Style.dialog.fontSize * Style.pt
}
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)
onLinkActivated : {
console.log("clicked link:", link)
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}
}
ButtonRounded{
anchors.horizontalCenter: parent.horizontalCenter
visible: root.hasError
text: qsTr("Close")
onClicked: root.cancel()
}
}
}
function clear() {
root.hasError = false
go.progress = 0.0
go.progressDescription = 0
}
function finished(hasError) {
root.hasError = hasError
root.incrementCurrentIndex()
}
onShow: {
root.clear()
}
onHide: {
root.clear()
}
onOkay: {
switch (root.currentIndex) {
case 0:
go.startUpdate()
}
root.incrementCurrentIndex()
}
onCancel: {
root.hide()
}
}

View File

@ -0,0 +1,78 @@
// Copyright (c) 2020 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/>.
// one line input text field with label
import QtQuick 2.8
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.0
import ProtonUI 1.0
Row {
id: root
spacing: Style.dialog.spacing
property string title : "title"
property alias path: inputPath.text
property alias inputPath: inputPath
property alias dialogVisible: pathDialog.visible
InputBox {
id: inputPath
anchors {
bottom: parent.bottom
}
spacing: Style.dialog.spacing
field {
height: browseButton.height
width: root.width - root.spacing - browseButton.width
}
label: title
Component.onCompleted: sanitizePath(pathDialog.shortcuts.home)
}
ButtonRounded {
id: browseButton
anchors {
bottom: parent.bottom
}
height: Style.dialog.heightInput
color_main: Style.main.textBlue
fa_icon: Style.fa.folder_open
text: qsTr("Browse", "click to look through directory for a file or folder")
onClicked: pathDialog.visible = true
}
FileDialog {
id: pathDialog
title: root.title + ":"
folder: shortcuts.home
onAccepted: sanitizePath(pathDialog.fileUrl.toString())
selectFolder: true
}
function sanitizePath(path) {
var pattern = "file://"
if (go.goos=="windows") pattern+="/"
inputPath.text = path.replace(pattern, "")
}
function checkNonEmpty() {
return inputPath.text != ""
}
}

View File

@ -0,0 +1,101 @@
// Copyright (c) 2020 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/>.
// on hover information
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
Text { // info icon
id:root
property alias info : tip.text
font {
family: Style.fontawesome.name
pointSize : Style.dialog.iconSize * Style.pt
}
text: Style.fa.info_circle
color: Style.main.textDisabled
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered : tip.visible=true
onExited : tip.visible=false
}
ToolTip {
id: tip
width: Style.bubble.width
x: - 0.2*tip.width
y: - tip.height
topPadding : Style.main.fontSize/2
bottomPadding : Style.main.fontSize/2
leftPadding : Style.bubble.widthPane + Style.dialog.spacing
rightPadding: Style.dialog.spacing
delay: 800
background : Rectangle {
id: bck
color: Style.bubble.paneBackground
radius : Style.bubble.radius
Text {
id: icon
color: Style.bubble.background
text: Style.fa.info_circle
font {
family : Style.fontawesome.name
pointSize : Style.dialog.iconSize * Style.pt
}
anchors {
verticalCenter : bck.verticalCenter
left : bck.left
leftMargin : (Style.bubble.widthPane - icon.width) / 2
}
}
Rectangle { // right edge
anchors {
fill : bck
leftMargin : Style.bubble.widthPane
}
radius: parent.radius
color: Style.bubble.background
}
Rectangle { // center background
anchors {
fill : parent
leftMargin : Style.bubble.widthPane
rightMargin : Style.bubble.widthPane
}
color: Style.bubble.background
}
}
contentItem : Text {
text: tip.text
color: Style.bubble.text
wrapMode: Text.Wrap
font.pointSize: Style.main.fontSize * Style.pt
}
}
}

View File

@ -0,0 +1,233 @@
// Copyright (c) 2020 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/>.
// Important information under title bar
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import ProtonUI 1.0
Rectangle {
id: root
property var iTry: 0
property var secLeft: 0
property var second: 1000 // convert millisecond to second
property var checkInterval: [ 5, 10, 30, 60, 120, 300, 600 ] // seconds
property bool isVisible: true
property var fontSize : 1.2 * Style.main.fontSize
color : "black"
state: "upToDate"
Timer {
id: retryInternet
interval: second
triggeredOnStart: false
repeat: true
onTriggered : {
secLeft--
if (secLeft <= 0) {
retryInternet.stop()
go.checkInternet()
if (iTry < checkInterval.length-1) {
iTry++
}
secLeft=checkInterval[iTry]
retryInternet.start()
}
}
}
Row {
anchors.centerIn: root
visible: root.isVisible
spacing: Style.main.leftMarginButton
AccessibleText {
id: message
font.pointSize: root.fontSize * Style.pt
}
ClickIconText {
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 {
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 {
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 {
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
}
}
onStateChanged : {
switch (root.state) {
case "forceUpdate" :
gui.warningFlags |= Style.errorInfoBar
break;
case "upToDate" :
gui.warningFlags &= ~Style.warnInfoBar
iTry = 0
secLeft=checkInterval[iTry]
break;
case "noInternet" :
gui.warningFlags |= Style.warnInfoBar
retryInternet.start()
secLeft=checkInterval[iTry]
break;
default :
gui.warningFlags |= Style.warnInfoBar
}
if (root.state!="noInternet") {
retryInternet.stop()
}
}
function timeToRetry() {
if (secLeft==1){
return qsTr("a second", "time to wait till internet connection is retried")
} else if (secLeft<60){
return secLeft + " " + qsTr("seconds", "time to wait till internet connection is retried")
} else {
var leading = ""+secLeft%60
if (leading.length < 2) {
leading = "0" + leading
}
return Math.floor(secLeft/60) + ":" + leading
}
}
states: [
State {
name: "internetCheck"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
isVisible: true
color: Style.main.textOrange
}
PropertyChanges {
target: message
color: Style.main.background
text: qsTr("Checking connection. Please wait...", "displayed after user retries internet connection")
}
},
State {
name: "noInternet"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
isVisible: true
color: Style.main.textRed
}
PropertyChanges {
target: message
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()+"."
}
},
State {
name: "oldVersion"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
isVisible: true
color: Style.main.textBlue
}
PropertyChanges {
target: message
color: Style.main.background
text: qsTr("An update is available.", "displayed in a notification when an app update is available")
}
},
State {
name: "forceUpdate"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
isVisible: true
color: Style.main.textRed
}
PropertyChanges {
target: message
color: Style.main.line
text: qsTr("%1 is outdated.", "displayed in a notification when app is outdated").arg(go.programTitle)
}
},
State {
name: "upToDate"
PropertyChanges {
target: root
height: 0
isVisible: false
color: Style.main.textBlue
}
PropertyChanges {
target: message
color: Style.main.background
text: ""
}
}
]
}

View File

@ -0,0 +1,78 @@
// Copyright (c) 2020 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/>.
// one line input text field with label
import QtQuick 2.8
import QtQuick.Controls 2.2
import QtQuick.Controls.Styles 1.4
import ProtonUI 1.0
import QtGraphicalEffects 1.0
Column {
id: root
property alias label: textlabel.text
property alias placeholderText: inputField.placeholderText
property alias echoMode: inputField.echoMode
property alias text: inputField.text
property alias field: inputField
signal accepted()
spacing: Style.dialog.heightSeparator
Text {
id: textlabel
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
color: Style.dialog.text
}
TextField {
id: inputField
width: Style.dialog.widthInput
height: Style.dialog.heightButton
selectByMouse : true
selectionColor : Style.main.textBlue
padding : Style.dialog.radiusButton
color : Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
family : Style.fontawesome.name
}
background: Rectangle {
color : Style.dialog.background
radius: Style.dialog.radiusButton
border {
color : Style.dialog.line
width : Style.dialog.borderInput
}
layer.enabled: true
layer.effect: FastBlur {
anchors.fill: parent
radius: 8 * Style.px
}
}
}
Connections {
target : inputField
onAccepted : root.accepted()
}
}

View File

@ -0,0 +1,172 @@
// Copyright (c) 2020 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/>.
// one line input text field with label
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
Column {
id: root
property alias focusInput : inputField.focus
property alias label : textlabel.text
property alias iconText : iconInput.text
property alias placeholderText : inputField.placeholderText
property alias text : inputField.text
property bool isPassword : false
property string rightIcon : ""
signal accepted()
signal editingFinished()
spacing: Style.dialog.heightSeparator
anchors.horizontalCenter : parent.horizontalCenter
AccessibleText {
id: textlabel
anchors.left : parent.left
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
horizontalAlignment: Text.AlignHCenter
color : Style.dialog.text
}
Rectangle {
id: inputWrap
anchors.horizontalCenter : parent.horizontalCenter
width : Style.dialog.widthInput
height : Style.dialog.heightInput
color : "transparent"
Text {
id: iconInput
anchors {
top : parent.top
left : parent.left
}
color : Style.dialog.text
font {
pointSize : Style.dialog.iconSize * Style.pt
family : Style.fontawesome.name
}
text: "o"
}
TextField {
id: inputField
anchors {
fill: inputWrap
leftMargin : Style.dialog.iconSize+Style.dialog.fontSize
bottomMargin : inputWrap.height - Style.dialog.iconSize
}
verticalAlignment : TextInput.AlignTop
horizontalAlignment : TextInput.AlignLeft
selectByMouse : true
color : Style.dialog.text
selectionColor : Style.main.textBlue
font {
pointSize : Style.dialog.fontSize * Style.pt
family : Style.fontawesome.name
}
padding: 0
background: Rectangle {
anchors.fill: parent
color : "transparent"
}
Component.onCompleted : {
if (isPassword) {
echoMode = TextInput.Password
} else {
echoMode = TextInput.Normal
}
}
Accessible.name: textlabel.text
Accessible.description: textlabel.text
}
Text {
id: iconRight
anchors {
top : parent.top
right : parent.right
}
color : Style.dialog.text
font {
pointSize : Style.dialog.iconSize * Style.pt
family : Style.fontawesome.name
}
text: ( !isPassword ? "" : (
inputField.echoMode == TextInput.Password ? Style.fa.eye : Style.fa.eye_slash
)) + " " + rightIcon
MouseArea {
anchors.fill: parent
onClicked: {
if (isPassword) {
if (inputField.echoMode == TextInput.Password) inputField.echoMode = TextInput.Normal
else inputField.echoMode = TextInput.Password
}
}
}
}
Rectangle {
anchors {
left : parent.left
right : parent.right
bottom : parent.bottom
}
height: Math.max(Style.main.border,1)
color: Style.dialog.text
}
}
function checkNonEmpty() {
if (inputField.text == "") {
rightIcon = Style.fa.exclamation_triangle
root.placeholderText = ""
inputField.focus = true
return false
} else {
rightIcon = Style.fa.check_circle
}
return true
}
function hidePasswordText() {
if (root.isPassword) inputField.echoMode = TextInput.Password
}
function forceFocus() {
inputField.forceActiveFocus()
}
Connections {
target: inputField
onAccepted: root.accepted()
onEditingFinished: root.editingFinished()
}
Keys.onPressed: {
if (event.key == Qt.Key_Enter) {
root.accepted()
}
}
}

View File

@ -0,0 +1,79 @@
// Copyright (c) 2020 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/>.
// This is main window
import QtQuick 2.8
import QtQuick.Window 2.2
import ProtonUI 1.0
// Main Window
Window {
id:winMain
// main window appeareance
width : Style.main.width
height : Style.main.height
flags : Qt.Window | Qt.Dialog
title: qsTr("ProtonMail Bridge", "app title")
color : Style.main.background
visible : true
Text {
id: title
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: Style.main.topMargin
}
font{
pointSize: Style.dialog.titleSize * Style.pt
}
color: Style.main.text
text:
"<span style='font-family: " + Style.fontawesome.name + "'>" + Style.fa.exclamation_triangle + "</span> " +
qsTr ("Warning: Instance exists", "displayed when a version of the app is opened while another is already running")
}
Text {
id: message
anchors.centerIn : parent
horizontalAlignment: Text.AlignHCenter
font.pointSize: Style.dialog.fontSize * Style.pt
color: Style.main.text
width: 2*parent.width/3
wrapMode: Text.Wrap
text: qsTr("An instance of the ProtonMail Bridge is already running.", "displayed when a version of the app is opened while another is already running") + " " +
qsTr("Please close the existing ProtonMail Bridge process before starting a new one.", "displayed when a version of the app is opened while another is already running")+ " " +
qsTr("This program will close now.", "displayed when a version of the app is opened while another is already running")
}
ButtonRounded {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: Style.main.bottomMargin
}
text: qsTr("Okay", "confirms and dismisses a notification")
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
onClicked: Qt.quit()
}
}

View File

@ -0,0 +1,150 @@
// Copyright (c) 2020 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/>.
// Header of window with logo and buttons
import QtQuick 2.8
import ProtonUI 1.0
import QtQuick.Window 2.2
Rectangle {
id: root
// dimensions
property Window parentWin
property string title: "ProtonMail Bridge"
property bool hasIcon : true
anchors.top : parent.top
anchors.right : parent.right
width : Style.main.width
height : Style.title.height
// style
color : Style.title.background
signal hideClicked()
// Drag to move : https://stackoverflow.com/a/18927884
MouseArea {
property variant clickPos: "1,1"
anchors.fill: parent
onPressed: {
clickPos = Qt.point(mouse.x,mouse.y)
}
onPositionChanged: {
var delta = Qt.point(mouse.x-clickPos.x, mouse.y-clickPos.y)
parentWin.x += delta.x;
parentWin.y += delta.y;
}
}
// logo
Image {
id: imgLogo
height : Style.title.imgHeight
fillMode : Image.PreserveAspectFit
visible: root.hasIcon
anchors {
left : root.left
leftMargin : Style.title.leftMargin
verticalCenter : root.verticalCenter
}
//source : "qrc://logo.svg"
source : "logo.svg"
smooth : true
}
TextMetrics {
id: titleMetrics
elideWidth: 2*root.width/3
elide: Qt.ElideMiddle
font: titleText.font
text: root.title
}
// Title
Text {
id: titleText
anchors {
left : hasIcon ? imgLogo.right : parent.left
leftMargin : hasIcon ? Style.title.leftMargin : Style.main.leftMargin
verticalCenter : root.verticalCenter
}
text : titleMetrics.elidedText
color : Style.title.text
font.pointSize : Style.title.fontSize * Style.pt
}
// Underline Button
Rectangle {
id: buttonUndrLine
anchors {
verticalCenter : root.verticalCenter
right : buttonCross.left
rightMargin : 2*Style.title.fontSize
}
width : Style.title.fontSize
height : Style.title.fontSize
color : "transparent"
Canvas {
anchors.fill: parent
onPaint: {
var val = Style.title.fontSize
var ctx = getContext("2d")
ctx.strokeStyle = 'white'
ctx.strokeWidth = 4
ctx.moveTo(0 , val-1)
ctx.lineTo(val, val-1)
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
onClicked: root.hideClicked()
}
}
// Cross Button
Rectangle {
id: buttonCross
anchors {
verticalCenter : root.verticalCenter
right : root.right
rightMargin : Style.main.rightMargin
}
width : Style.title.fontSize
height : Style.title.fontSize
color : "transparent"
Canvas {
anchors.fill: parent
onPaint: {
var val = Style.title.fontSize
var ctx = getContext("2d")
ctx.strokeStyle = 'white'
ctx.strokeWidth = 4
ctx.moveTo(0,0)
ctx.lineTo(val,val)
ctx.moveTo(val,0)
ctx.lineTo(0,val)
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
onClicked: root.hideClicked()
}
}
}

View File

@ -0,0 +1,81 @@
// Copyright (c) 2020 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/>.
// Popup message
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
Rectangle {
id: root
color: Style.transparent
property alias text: message.text
visible: false
MouseArea { // prevent action below
anchors.fill: parent
hoverEnabled: true
}
Rectangle {
id: backgroundInp
anchors.centerIn : root
color : Style.errorDialog.background
radius : Style.errorDialog.radius
width : parent.width/3.
height : contentInp.height
Column {
id: contentInp
anchors.horizontalCenter: backgroundInp.horizontalCenter
spacing: Style.dialog.heightSeparator
topPadding: Style.dialog.heightSeparator
bottomPadding: Style.dialog.heightSeparator
AccessibleText {
id: message
font {
pointSize : Style.errorDialog.fontSize * Style.pt
bold : true
}
color: Style.errorDialog.text
horizontalAlignment: Text.AlignHCenter
width : backgroundInp.width - 2*Style.main.rightMargin
wrapMode: Text.Wrap
}
ButtonRounded {
text : qsTr("Okay", "todo")
isOpaque : true
color_main : Style.dialog.background
color_minor : Style.dialog.textBlue
onClicked : root.hide()
anchors.horizontalCenter : parent.horizontalCenter
}
}
}
function show(text) {
root.text = text
root.visible = true
}
function hide() {
root.state = "Okay"
root.visible=false
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2020 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/>.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,69 @@
// Copyright (c) 2020 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/>.
// Important information under title bar
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import ProtonUI 1.0
Rectangle {
id: root
height: 0
visible: state != "ok"
state: "ok"
color: "black"
property var fontSize : 1.0 * Style.main.fontSize
Row {
anchors.centerIn: root
visible: root.visible
spacing: Style.main.leftMarginButton
AccessibleText {
id: message
font.pointSize: root.fontSize * Style.pt
text: qsTr("Connection security error: Your network connection to Proton services may be insecure.", "message in bar showed when TLS Pinning fails")
}
ClickIconText {
anchors.verticalCenter : message.verticalCenter
iconText : ""
text : qsTr("Learn more", "This button opens TLS Pinning issue modal with more explanation")
visible : root.visible
onClicked : {
winMain.dialogTlsCert.show()
}
fontSize : root.fontSize
textUnderline: true
}
}
states: [
State {
name: "notOK"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
color: Style.main.textRed
}
}
]
}

View File

@ -0,0 +1,103 @@
// Copyright (c) 2020 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/>.
// Button with text and icon for tabbar
import QtQuick 2.8
import ProtonUI 1.0
import QtQuick.Controls 2.1
AccessibleButton {
id: root
property alias iconText : icon.text
property alias title : titleText.text
property color textColor : {
if (root.state=="deactivated") {
return Qt.lighter(Style.tabbar.textInactive, root.hovered || root.activeFocus ? 1.25 : 1.0)
}
if (root.state=="activated") {
return Style.tabbar.text
}
}
text: root.title
Accessible.description: root.title + " tab"
width : titleMetrics.width // Style.tabbar.widthButton
height : Style.tabbar.heightButton
padding: 0
background: Rectangle {
color : Style.transparent
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.NoButton
}
}
contentItem : Rectangle {
color: "transparent"
scale : root.pressed ? 0.96 : 1.00
Text {
id: icon
// dimenstions
anchors {
top : parent.top
horizontalCenter : parent.horizontalCenter
}
// style
color : root.textColor
font {
family : Style.fontawesome.name
pointSize : Style.tabbar.iconSize * Style.pt
}
}
TextMetrics {
id: titleMetrics
text : root.title
font.pointSize : titleText.font.pointSize
}
Text {
id: titleText
// dimenstions
anchors {
bottom : parent.bottom
horizontalCenter : parent.horizontalCenter
}
// style
color : root.textColor
font {
pointSize : Style.tabbar.fontSize * Style.pt
bold : root.state=="activated"
}
}
}
states: [
State {
name: "activated"
},
State {
name: "deactivated"
}
]
}

View File

@ -0,0 +1,107 @@
// Copyright (c) 2020 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/>.
// Tab labels
import QtQuick 2.8
import ProtonUI 1.0
Rectangle {
id: root
// attributes
property alias model : tablist.model
property alias currentIndex : tablist.currentIndex
property int spacing : Style.tabbar.widthButton + Style.tabbar.spacingButton
currentIndex: 0
// appereance
height : Style.tabbar.height
color : Style.tabbar.background
// content
ListView {
id: tablist
// dimensions
anchors {
fill: root
leftMargin : Style.tabbar.leftMargin
rightMargin : Style.main.rightMargin
bottomMargin : Style.tabbar.bottomMargin
}
spacing: Style.tabbar.spacingButton
interactive : false
// style
orientation: Qt.Horizontal
delegate: TabButton {
anchors.bottom : parent.bottom
title : modelData.title
iconText : modelData.iconText
state : index == tablist.currentIndex ? "activated" : "deactivated"
onClicked : {
tablist.currentIndex = index
}
}
}
// Quit button
TabButton {
id: buttonQuit
title : qsTr("Close Bridge", "quits the application")
iconText : Style.fa.power_off
state : "deactivated"
visible : Style.tabbar.rightButton=="quit"
anchors {
right : root.right
bottom : root.bottom
rightMargin : Style.main.rightMargin
bottomMargin : Style.tabbar.bottomMargin
}
Accessible.description: buttonQuit.title
onClicked : {
dialogGlobal.state = "quit"
dialogGlobal.show()
}
}
// Add account
TabButton {
id: buttonAddAccount
title : qsTr("Add account", "start the authentication to add account")
iconText : Style.fa.plus_circle
state : "deactivated"
visible : Style.tabbar.rightButton=="add account"
anchors {
right : root.right
bottom : root.bottom
rightMargin : Style.main.rightMargin
bottomMargin : Style.tabbar.bottomMargin
}
Accessible.description: buttonAddAccount.title
onClicked : dialogAddUser.show()
}
function focusButton() {
tablist.currentItem.forceActiveFocus()
tablist.currentItem.Accessible.focusedChanged(true)
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) 2020 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/>.
import QtQuick 2.8
import ProtonUI 1.0
AccessibleText{
id: root
property bool hasCopyButton : false
font.pointSize: Style.main.fontSize * Style.pt
state: "label"
states : [
State {
name: "label"
PropertyChanges {
target : root
font.bold : false
color : Style.main.textDisabled
}
},
State {
name: "heading"
PropertyChanges {
target : root
font.bold : true
color : Style.main.textDisabled
}
}
]
}

View File

@ -0,0 +1,85 @@
// Copyright (c) 2020 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/>.
import QtQuick 2.8
import ProtonUI 1.0
Rectangle {
id: root
property string text: "undef"
width: copyIcon.width + valueText.width
height: Math.max(copyIcon.height, valueText.contentHeight)
color: "transparent"
Rectangle {
id: copyIcon
width: Style.info.leftMarginIcon*2 + Style.info.iconSize
height : Style.info.iconSize
color: "transparent"
anchors {
top: root.top
left: root.left
}
Text {
anchors.centerIn: parent
font {
pointSize : Style.info.iconSize * Style.pt
family : Style.fontawesome.name
}
color : Style.main.textInactive
text: Style.fa.copy
}
MouseArea {
anchors.fill: parent
onClicked : {
valueText.select(0, valueText.length)
valueText.copy()
valueText.deselect()
}
onPressed: copyIcon.scale = 0.90
onReleased: copyIcon.scale = 1
}
Accessible.role: Accessible.Button
Accessible.name: qsTr("Copy %1 to clipboard", "Click to copy the value to system clipboard.").arg(root.text)
Accessible.description: Accessible.name
}
TextEdit {
id: valueText
width: Style.info.widthValue
height: Style.main.fontSize
anchors {
top: root.top
left: copyIcon.right
}
font {
pointSize: Style.main.fontSize * Style.pt
}
color: Style.main.text
readOnly: true
selectByMouse: true
selectByKeyboard: true
wrapMode: TextEdit.Wrap
text: root.text
selectionColor: Style.dialog.textBlue
Accessible.role: Accessible.StaticText
Accessible.name: root.text
Accessible.description: Accessible.name
}
}

View File

@ -0,0 +1,348 @@
// Copyright (c) 2020 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/>.
// simulating window title bar with different color
import QtQuick 2.8
import QtQuick.Window 2.2
import ProtonUI 1.0
Rectangle {
id: root
height: root.isDarwin ? Style.titleMacOS.height : Style.title.height
color: "transparent"
property bool isDarwin : (go.goos == "darwin")
property QtObject window
anchors {
left : parent.left
right : parent.right
top : parent.top
}
MouseArea {
property point diff: "0,0"
anchors {
top: root.top
bottom: root.bottom
left: root.left
right: root.isDarwin ? root.right : iconRowWin.left
}
onPressed: {
diff = Qt.point(window.x, window.y)
var mousePos = mapToGlobal(mouse.x, mouse.y)
diff.x -= mousePos.x
diff.y -= mousePos.y
}
onPositionChanged: {
var currPos = mapToGlobal(mouse.x, mouse.y)
window.x = currPos.x + diff.x
window.y = currPos.y + diff.y
}
}
// top background
Rectangle {
id: upperBackground
anchors.fill: root
color: (isDarwin? Style.titleMacOS.background : Style.title.background )
radius: (isDarwin? Style.titleMacOS.radius : 0)
border {
width: Style.main.border
color: Style.title.background
}
}
// bottom background
Rectangle {
id: lowerBorder
anchors {
top: root.verticalCenter
left: root.left
right: root.right
bottom: root.bottom
}
color: Style.title.background
Rectangle {
id: lowerBackground
anchors{
fill : parent
leftMargin : Style.main.border
rightMargin : Style.main.border
}
color: upperBackground.color
}
}
// Title
TextMetrics {
id: titleMetrics
text : window.title
font : isDarwin ? titleMac.font : titleWin.font
elide: Qt.ElideMiddle
elideWidth : window.width/2
}
Text {
id: titleWin
visible: !isDarwin
anchors {
baseline : logo.bottom
left : logo.right
leftMargin : Style.title.leftMargin/1.5
}
color : window.active ? Style.title.text : Style.main.textDisabled
text : titleMetrics.elidedText
font.pointSize : Style.main.fontSize * Style.pt
}
Text {
id: titleMac
visible: isDarwin
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : (parent.width-width)/2
}
color : window.active ? Style.title.text : Style.main.textDisabled
text : titleMetrics.elidedText
font.pointSize : Style.main.fontSize * Style.pt
}
// MACOS
MouseArea {
anchors.fill: iconRowMac
property string beforeHover
hoverEnabled: true
onEntered: {
beforeHover=iconRed.state
//iconYellow.state="hover"
iconRed.state="hover"
}
onExited: {
//iconYellow.state=beforeHover
iconRed.state=beforeHover
}
}
Connections {
target: window
onActiveChanged : {
if (window.active) {
//iconYellow.state="normal"
iconRed.state="normal"
} else {
//iconYellow.state="disabled"
iconRed.state="disabled"
}
}
}
Row {
id: iconRowMac
visible : isDarwin
spacing : Style.titleMacOS.leftMargin
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
leftMargin : Style.title.leftMargin
}
Image {
id: iconRed
width : Style.titleMacOS.imgHeight
height : Style.titleMacOS.imgHeight
fillMode : Image.PreserveAspectFit
smooth : true
state : "normal"
states: [
State { name: "normal" ; PropertyChanges { target: iconRed ; source: "images/macos_red.png" } },
State { name: "hover" ; PropertyChanges { target: iconRed ; source: "images/macos_red_hl.png" } },
State { name: "pressed" ; PropertyChanges { target: iconRed ; source: "images/macos_red_dark.png" } },
State { name: "disabled" ; PropertyChanges { target: iconRed ; source: "images/macos_gray.png" } }
]
MouseArea {
anchors.fill: parent
property string beforePressed : "normal"
onClicked : {
window.close()
}
onPressed: {
beforePressed = parent.state
parent.state="pressed"
}
onReleased: {
parent.state=beforePressed
}
Accessible.role: Accessible.Button
Accessible.name: qsTr("Close", "Close the window button")
Accessible.description: Accessible.name
Accessible.ignored: !parent.visible
Accessible.onPressAction: {
window.close()
}
}
}
Image {
id: iconYellow
width : Style.titleMacOS.imgHeight
height : Style.titleMacOS.imgHeight
fillMode : Image.PreserveAspectFit
smooth : true
state : "disabled"
states: [
State { name: "normal" ; PropertyChanges { target: iconYellow ; source: "images/macos_yellow.png" } },
State { name: "hover" ; PropertyChanges { target: iconYellow ; source: "images/macos_yellow_hl.png" } },
State { name: "pressed" ; PropertyChanges { target: iconYellow ; source: "images/macos_yellow_dark.png" } },
State { name: "disabled" ; PropertyChanges { target: iconYellow ; source: "images/macos_gray.png" } }
]
/*
MouseArea {
anchors.fill: parent
property string beforePressed : "normal"
onClicked : {
window.visibility = Window.Minimized
}
onPressed: {
beforePressed = parent.state
parent.state="pressed"
}
onReleased: {
parent.state=beforePressed
}
Accessible.role: Accessible.Button
Accessible.name: qsTr("Minimize", "Minimize the window button")
Accessible.description: Accessible.name
Accessible.ignored: !parent.visible
Accessible.onPressAction: {
window.visibility = Window.Minimized
}
}
*/
}
Image {
id: iconGreen
width : Style.titleMacOS.imgHeight
height : Style.titleMacOS.imgHeight
fillMode : Image.PreserveAspectFit
smooth : true
source : "images/macos_gray.png"
Component.onCompleted : {
visible = false // (window.flags&Qt.Dialog) != Qt.Dialog
}
}
}
// Windows
Image {
id: logo
visible: !isDarwin
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
leftMargin : Style.title.leftMargin
}
height : Style.title.fontSize-2*Style.px
fillMode : Image.PreserveAspectFit
mipmap : true
source : "images/pm_logo.png"
}
Row {
id: iconRowWin
visible: !isDarwin
anchors {
right : parent.right
verticalCenter : root.verticalCenter
}
Rectangle {
height : root.height
width : 1.5*height
color: Style.transparent
Image {
id: iconDash
anchors.centerIn: parent
height : iconTimes.height*0.90
fillMode : Image.PreserveAspectFit
mipmap : true
source : "images/win10_Dash.png"
}
MouseArea {
anchors.fill : parent
hoverEnabled : true
onClicked : {
window.visibility = Window.Minimized
}
onPressed: {
parent.scale=0.92
}
onReleased: {
parent.scale=1
}
onEntered: {
parent.color= Qt.lighter(Style.title.background,1.2)
}
onExited: {
parent.color=Style.transparent
}
Accessible.role : Accessible.Button
Accessible.name : qsTr("Minimize", "Minimize the window button")
Accessible.description : Accessible.name
Accessible.ignored : !parent.visible
Accessible.onPressAction : {
window.visibility = Window.Minimized
}
}
}
Rectangle {
height : root.height
width : 1.5*height
color : Style.transparent
Image {
id: iconTimes
anchors.centerIn : parent
mipmap : true
height : parent.height/1.5
fillMode : Image.PreserveAspectFit
source : "images/win10_Times.png"
}
MouseArea {
anchors.fill : parent
hoverEnabled : true
onClicked : window.close()
onPressed : {
iconTimes.scale=0.92
}
onReleased: {
parent.scale=1
}
onEntered: {
parent.color=Style.main.textRed
}
onExited: {
parent.color=Style.transparent
}
Accessible.role : Accessible.Button
Accessible.name : qsTr("Close", "Close the window button")
Accessible.description : Accessible.name
Accessible.ignored : !parent.visible
Accessible.onPressAction : {
window.close()
}
}
}
}
}

View File

@ -0,0 +1,31 @@
module ProtonUI
singleton Style 1.0 Style.qml
AccessibleButton 1.0 AccessibleButton.qml
AccessibleText 1.0 AccessibleText.qml
AccessibleSelectableText 1.0 AccessibleSelectableText.qml
AccountView 1.0 AccountView.qml
AddAccountBar 1.0 AddAccountBar.qml
BubbleNote 1.0 BubbleNote.qml
BugReportWindow 1.0 BugReportWindow.qml
ButtonIconText 1.0 ButtonIconText.qml
ButtonRounded 1.0 ButtonRounded.qml
CheckBoxLabel 1.0 CheckBoxLabel.qml
ClickIconText 1.0 ClickIconText.qml
Dialog 1.0 Dialog.qml
DialogAddUser 1.0 DialogAddUser.qml
DialogUpdate 1.0 DialogUpdate.qml
DialogConnectionTroubleshoot 1.0 DialogConnectionTroubleshoot.qml
FileAndFolderSelect 1.0 FileAndFolderSelect.qml
InfoToolTip 1.0 InfoToolTip.qml
InformationBar 1.0 InformationBar.qml
InputBox 1.0 InputBox.qml
InputField 1.0 InputField.qml
InstanceExistsWindow 1.0 InstanceExistsWindow.qml
LogoHeader 1.0 LogoHeader.qml
PopupMessage 1.0 PopupMessage.qml
TabButton 1.0 TabButton.qml
TabLabels 1.0 TabLabels.qml
TextLabel 1.0 TextLabel.qml
TextValue 1.0 TextValue.qml
TLSCertPinIssueBar 1.0 TLSCertPinIssueBar.qml
WindowTitleBar 1.0 WindowTitleBar.qml

View File

@ -0,0 +1,611 @@
// Copyright (c) 2020 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/>.
import QtQuick 2.8
import QtTest 1.2
import BridgeUI 1.0
import ProtonUI 1.0
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
Window {
id: testroot
width : 150
height : 600
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
visible : true
title : "GUI test Window"
color : "transparent"
property bool newVersion : true
Accessible.name: testroot.title
Accessible.description: "Window with buttons testing the GUI events"
Rectangle {
id:test_systray
anchors{
top: parent.top
horizontalCenter: parent.horizontalCenter
}
height: 40
width: testroot.width
color: "yellow"
Image {
id: sysImg
anchors {
left : test_systray.left
top : test_systray.top
}
height: test_systray.height
mipmap: true
fillMode : Image.PreserveAspectFit
source: ""
}
Text {
id: systrText
anchors {
right : test_systray.right
verticalCenter: test_systray.verticalCenter
}
text: "unset"
}
function normal() {
test_systray.color = "#22ee22"
systrText.text = "norm"
sysImg.source= "../share/icons/black-systray.png"
}
function highlight() {
test_systray.color = "#eeee22"
systrText.text = "highl"
sysImg.source= "../share/icons/black-syswarn.png"
}
function error() {
test_systray.color = "#ee2222"
systrText.text = "error"
sysImg.source= "../share/icons/black-syserror.png"
}
MouseArea {
property point diff: "0,0"
anchors.fill: parent
onPressed: {
diff = Qt.point(testroot.x, testroot.y)
var mousePos = mapToGlobal(mouse.x, mouse.y)
diff.x -= mousePos.x
diff.y -= mousePos.y
}
onPositionChanged: {
var currPos = mapToGlobal(mouse.x, mouse.y)
testroot.x = currPos.x + diff.x
testroot.y = currPos.y + diff.y
}
}
}
ListModel {
id: buttons
ListElement { title: "Show window" }
ListElement { title: "Show help" }
ListElement { title: "Show quit" }
ListElement { title: "Logout bridge" }
ListElement { title: "Internet on" }
ListElement { title: "Internet off" }
ListElement { title: "NeedUpdate" }
ListElement { title: "UpToDate" }
ListElement { title: "ForceUpdate" }
ListElement { title: "Linux" }
ListElement { title: "Windows" }
ListElement { title: "Macos" }
ListElement { title: "FirstDialog" }
ListElement { title: "AutostartError" }
ListElement { title: "BusyPortIMAP" }
ListElement { title: "BusyPortSMTP" }
ListElement { title: "BusyPortBOTH" }
ListElement { title: "Minimize this" }
ListElement { title: "SendAlertPopup" }
ListElement { title: "TLSCertError" }
}
ListView {
id: view
anchors {
top : test_systray.bottom
bottom : parent.bottom
left : parent.left
right : parent.right
}
orientation : ListView.Vertical
model : buttons
focus : true
delegate : ButtonRounded {
text : title
color_main : "orange"
color_minor : "#aa335588"
isOpaque : true
width: testroot.width
height : 20*Style.px
anchors.horizontalCenter: parent.horizontalCenter
onClicked : {
console.log("Clicked on ", title)
switch (title) {
case "Show window" :
go.showWindow();
break;
case "Show help" :
go.showHelp();
break;
case "Show quit" :
go.showQuit();
break;
case "Logout bridge" :
go.checkLoggedOut("bridge");
break;
case "Internet on" :
go.setConnectionStatus(true);
break;
case "Internet off" :
go.setConnectionStatus(false);
break;
case "Linux" :
go.goos = "linux";
break;
case "Macos" :
go.goos = "darwin";
break;
case "Windows" :
go.goos = "windows";
break;
case "FirstDialog" :
testgui.winMain.dialogFirstStart.show();
break;
case "AutostartError" :
go.notifyBubble(1,go.failedAutostart);
break;
case "BusyPortIMAP" :
go.notifyPortIssue(true,false);
break;
case "BusyPortSMTP" :
go.notifyPortIssue(false,true);
break;
case "BusyPortBOTH" :
go.notifyPortIssue(true,true);
break;
case "Minimize this" :
testroot.visibility = Window.Minimized
break;
case "UpToDate" :
testroot.newVersion = false
break;
case "NeedUpdate" :
testroot.newVersion = true
break;
case "ForceUpdate" :
go.notifyUpdate()
break;
case "SendAlertPopup" :
go.showOutgoingNoEncPopup("Alert sending unencrypted!")
break;
case "TLSCertError" :
go.showCertIssue()
break;
default :
console.log("Not implemented " + data)
}
}
}
}
Component.onCompleted : {
testroot.x= 10
testroot.y= 100
}
//InstanceExistsWindow { id: ie_test }
Gui {
id: testgui
ListModel{
id: accountsModel
ListElement{ account : "bridge" ; status : "connected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "bridge@pm.com;bridge2@pm.com;theHorriblySlowMurderWithExtremelyInefficientWeapon@youtube.com" }
ListElement{ account : "exteremelongnamewhichmustbeeladed@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "bridge@pm.com;bridge2@pm.com;hu@hu.hu" }
ListElement{ account : "bridge2@protonmail.com" ; status : "disconnected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "bridge@pm.com;bridge2@pm.com;hu@hu.hu" }
}
Component.onCompleted : {
winMain.x = testroot.x + testroot.width
winMain.y = testroot.y
}
}
QtObject {
id: go
property bool isAutoStart : true
property bool isProxyAllowed : false
property bool isFirstStart : false
property bool isFreshVersion : false
property bool isOutdateVersion : true
property string currentAddress : "none"
//property string goos : "windows"
property string goos : "linux"
////property string goos : "darwin"
property bool isDefaultPort : false
property bool isShownOnStart : true
property bool hasNoKeychain : true
property string wrongCredentials
property string wrongMailboxPassword
property string canNotReachAPI
property string versionCheckFailed
property string credentialsNotRemoved
property string bugNotSent
property string bugReportSent
property string failedAutostartPerm
property string failedAutostart
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 credits : "here;goes;list;;of;;used;packages;"
property real progress: 0.3
property int progressDescription: 2
signal toggleMainWin(int systX, int systY, int systW, int systH)
signal showWindow()
signal showHelp()
signal showQuit()
signal notifyPortIssue(bool busyPortIMAP, bool busyPortSMTP)
signal notifyVersionIsTheLatest()
signal setUpdateState(string updateState)
signal notifyKeychainRebuild()
signal notifyHasNoKeychain()
signal processFinished()
signal toggleAutoStart()
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 showCertIssue()
signal updateFinished(bool hasError)
signal showOutgoingNoEncPopup(string subject)
signal setOutgoingNoEncPopupCoord(real x, real y)
signal showNoActiveKeyForRecipient(string recipient)
function delay(duration) {
var timeStart = new Date().getTime();
while (new Date().getTime() - timeStart < duration) {
// Do nothing
}
}
function getLastMailClient() {
return "Mutt is the best"
}
function sendBug(desc,client,address){
console.log("bug report ", "desc '"+desc+"'", "client '"+client+"'", "address '"+address+"'")
return !desc.includes("fail")
}
function deleteAccount(index,remove) {
console.log ("Test: Delete account ",index," and remove prefences "+remove)
workAndClose()
accountsModel.remove(index)
}
function logoutAccount(index) {
accountsModel.get(index).status="disconnected"
workAndClose()
}
function login(username,password) {
delay(700)
if (password=="wrong") {
setAddAccountWarning("Wrong password")
return -1
}
if (username=="2fa") {
return 1
}
if (username=="mbox") {
return 2
}
return 0
}
function auth2FA(twoFACode){
delay(700)
if (twoFACode=="wrong") {
setAddAccountWarning("Wrong 2FA")
return -1
}
if (twoFACode=="mbox") {
return 1
}
return 0
}
function addAccount(mailboxPass) {
delay(700)
if (mailboxPass=="wrong") {
setAddAccountWarning("Wrong mailbox password")
return -1
}
accountsModel.append({
"account" : testgui.winMain.dialogAddUser.username,
"status" : "connected",
"isExpanded":true,
"hostname" : "127.0.0.1",
"password" : "ZI9tKp+ryaxmbpn2E12",
"security" : "StarTLS",
"portSMTP" : 1025,
"portIMAP" : 1143,
"aliases" : "bridge@pm.com;bridges@pm.com;theHorriblySlowMurderWithExtremelyInefficientWeapon@youtube.com",
"isCombinedAddressMode": true
})
workAndClose()
}
function checkInternet() {
var delay = Qt.createQmlObject("import QtQuick 2.8; Timer{}",go)
delay.interval = 2000
delay.repeat = false
delay.triggered.connect(function(){ go.setConnectionStatus(false) })
delay.start()
}
property SequentialAnimation animateProgressBar : SequentialAnimation {
// version
PropertyAnimation{ target: go; properties: "progressDescription"; to: 1; duration: 1; }
PropertyAnimation{ duration: 2000; }
// download
PropertyAnimation{ target: go; properties: "progressDescription"; to: 2; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.01; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.1; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.3; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.5; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.8; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 1.0; duration: 1; }
PropertyAnimation{ duration: 1000; }
// verify
PropertyAnimation{ target: go; properties: "progress"; to: 0.0; duration: 1; }
PropertyAnimation{ target: go; properties: "progressDescription"; to: 3; duration: 1; }
PropertyAnimation{ duration: 2000; }
// unzip
PropertyAnimation{ target: go; properties: "progressDescription"; to: 4; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.01; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.1; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.3; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.5; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.8; duration: 1; }
PropertyAnimation{ duration: 500; }
PropertyAnimation{ target: go; properties: "progress"; to: 1.0; duration: 1; }
PropertyAnimation{ duration: 2000; }
// update
PropertyAnimation{ target: go; properties: "progress"; to: 0.0; duration: 1; }
PropertyAnimation{ target: go; properties: "progressDescription"; to: 5; duration: 1; }
PropertyAnimation{ duration: 2000; }
// quit
PropertyAnimation{ target: go; properties: "progressDescription"; to: 6; duration: 1; }
PropertyAnimation{ duration: 2000; }
}
property Timer timer : Timer {
id: timer
interval : 700
repeat : false
property string work
onTriggered : {
console.log("triggered "+timer.work)
switch (timer.work) {
case "wait":
break
case "startUpdate":
go.animateProgressBar.start()
go.updateFinished(true)
default:
go.processFinished()
}
}
}
function workAndClose() {
timer.work="default"
timer.start()
}
function startUpdate() {
timer.work="startUpdate"
timer.start()
}
function loadAccounts() {
console.log("Test: Account loaded")
}
function openDownloadLink(){
}
function switchAddressMode(username){
for (var iAcc=0; iAcc < accountsModel.count; iAcc++) {
if (accountsModel.get(iAcc).account == username ) {
accountsModel.get(iAcc).isCombinedAddressMode = !accountsModel.get(iAcc).isCombinedAddressMode
break
}
}
workAndClose()
}
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()
}
function getBackendVersion() {
return "BridgeUI 1.0"
}
property bool isConnectionOK : true
signal setConnectionStatus(bool isAvailable)
function configureAppleMail(iAccount,iAddress) {
console.log ("Test: autoconfig account ",iAccount," address ",iAddress)
}
function openLogs() {
Qt.openUrlExternally("file:///home/dev/")
}
function highlightSystray() {
test_systray.highlight()
}
function errorSystray() {
test_systray.error()
}
function normalSystray() {
test_systray.normal()
}
signal bubbleClosed()
function getIMAPPort() {
return 1143
}
function getSMTPPort() {
return 1025
}
function isPortOpen(portstring){
if (isNaN(portstring)) {
return 1
}
var portnum = parseInt(portstring,10)
if (portnum < 3333) {
return 1
}
return 0
}
property bool isRestarting: false
function setPortsAndSecurity(portIMAP, portSMTP, secSMTP) {
console.log("Test: ports changed", portIMAP, portSMTP, secSMTP)
}
function isSMTPSTARTTLS() {
return true
}
signal openManual()
function clearCache() {
workAndClose()
}
function clearKeychain() {
workAndClose()
}
property bool isReportingOutgoingNoEnc : true
function toggleIsReportingOutgoingNoEnc() {
go.isReportingOutgoingNoEnc = !go.isReportingOutgoingNoEnc
console.log("Reporting changed to ", go.isReportingOutgoingNoEnc)
}
function saveOutgoingNoEncPopupCoord(x,y) {
console.log("Triggered saveOutgoingNoEncPopupCoord: ",x,y)
}
function shouldSendAnswer (messageID, shouldSend) {
if (shouldSend) console.log("answered to send email")
else console.log("answered to cancel email")
}
onToggleAutoStart: {
workAndClose()
isAutoStart = (isAutoStart!=false) ? false : true
console.log (" Test: toggleAutoStart "+isAutoStart)
}
}
}

View File

@ -0,0 +1,64 @@
QMLfiles=$(shell find ../qml/ -name "*.qml") $(shell find ../qml/ -name "qmldir")
FontAwesome=${CURDIR}/../share/fontawesome-webfont.ttf
ImageDir=${CURDIR}/../share/icons
Icons=$(shell find ${ImageDir} -name "*.png")
all: qmlcheck moc.go rcc.cpp logo.ico
deploy:
qtdeploy build desktop
../qml/ProtonUI/fontawesome.ttf:
ln -sf ${FontAwesome} $@
../qml/ProtonUI/images:
ln -sf ${ImageDir} $@
translate.ts: ${QMLfiles}
lupdate -recursive qml/ -ts $@
rcc.cpp: ${QMLfiles} ${Icons} resources.qrc
rm -f rcc.cpp rcc.qrc && qtrcc -o .
qmltest:
qmltestrunner -eventdelay 500 -import ./qml/
qmlcheck : ../qml/ProtonUI/fontawesome.ttf ../qml/ProtonUI/images
qmlscene -I ../qml/ -f ../qml/tst_Gui.qml --quit
qmlpreview : ../qml/ProtonUI/fontawesome.ttf ../qml/ProtonUI/images
rm -f ../qml/*.qmlc ../qml/BridgeUI/*.qmlc
qmlscene -verbose -I ../qml/ -f ../qml/tst_Gui.qml
#qmlscene -qmljsdebugger=port:3768,block -verbose -I ../qml/ -f ../qml/tst_Gui.qml
logo.ico: ../share/icons/logo.ico
cp $^ $@
test: qmlcheck moc.go rcc.cpp
go test -v -tags=cli
moc.go: ui.go accountModel.go
qtmoc
distclean: clean
rm -rf rcc_cgo*.go
clean:
rm -rf linux/
rm -rf darwin/
rm -rf windows/
rm -rf deploy/
rm -f logo.ico
rm -f moc.cpp
rm -f moc.go
rm -f moc.h
rm -f moc_cgo*.go
rm -f moc_moc.h
rm -f rcc.cpp
rm -f rcc.qrc
rm -f rcc_cgo*.go
rm -f ../rcc.cpp
rm -f ../rcc.qrc
rm -f ../rcc_cgo*.go
rm -rf ../qml/ProtonUI/images
rm -f ../qml/ProtonUI/fontawesome.ttf
find ../qml -name *.qmlc -exec rm {} \;

View File

@ -0,0 +1,240 @@
// Copyright (c) 2020 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/>.
// +build !nogui
package qt
import (
"fmt"
"github.com/therecipe/qt/core"
)
// The element of model.
// It contains all data for one account and its aliases.
type AccountInfo struct {
core.QObject
_ string `property:"account"`
_ string `property:"userID"`
_ string `property:"status"`
_ string `property:"hostname"`
_ string `property:"password"`
_ string `property:"security"` // Deprecated, not used.
_ int `property:"portSMTP"`
_ int `property:"portIMAP"`
_ string `property:"aliases"`
_ bool `property:"isExpanded"`
_ bool `property:"isCombinedAddressMode"`
}
// Constants for data map.
// enum-like constants in Go.
const (
Account = int(core.Qt__UserRole) + 1<<iota
UserID
Status
Hostname
Password
Security
PortIMAP
PortSMTP
Aliases
IsExpanded
IsCombinedAddressMode
)
// Registration of new metatype before creating instance.
// NOTE: check it is run once per program. Write a log.
func init() {
AccountInfo_QRegisterMetaType()
}
// Model for providing container for items (account information) to QML.
//
// QML ListView connects the model from Go and it shows item (accounts) information.
//
// Copied and edited from `github.com/therecipe/qt/internal/examples/sailfish/listview`.
type AccountsModel struct {
core.QAbstractListModel
// QtObject Constructor
_ func() `constructor:"init"`
// List of item properties.
// All available item properties are inside the map.
_ map[int]*core.QByteArray `property:"roles"`
// The data storage.
// The slice with all accounts. It is not accessed directly but using `data(index,role)`.
_ []*AccountInfo `property:"accounts"`
// Method for adding account.
_ func(*AccountInfo) `slot:"addAccount"`
// Method for retrieving account.
_ func(row int) *AccountInfo `slot:"get"`
// Method for login/logout the account.
_ func(row int) `slot:"toggleIsAvailable"`
// Method for removing account from list.
_ func(row int) `slot:"removeAccount"`
_ int `property:"count"`
}
// init is basically the constructor.
// Creates the map for item properties and connects the methods.
func (s *AccountsModel) init() {
s.SetRoles(map[int]*core.QByteArray{
Account: NewQByteArrayFromString("account"),
UserID: NewQByteArrayFromString("userID"),
Status: NewQByteArrayFromString("status"),
Hostname: NewQByteArrayFromString("hostname"),
Password: NewQByteArrayFromString("password"),
Security: NewQByteArrayFromString("security"),
PortIMAP: NewQByteArrayFromString("portIMAP"),
PortSMTP: NewQByteArrayFromString("portSMTP"),
Aliases: NewQByteArrayFromString("aliases"),
IsExpanded: NewQByteArrayFromString("isExpanded"),
IsCombinedAddressMode: NewQByteArrayFromString("isCombinedAddressMode"),
})
// Basic QAbstractListModel methods.
s.ConnectData(s.data)
s.ConnectRowCount(s.rowCount)
s.ConnectColumnCount(s.columnCount)
s.ConnectRoleNames(s.roleNames)
// Custom AccountModel methods.
s.ConnectGet(s.get)
s.ConnectAddAccount(s.addAccount)
s.ConnectToggleIsAvailable(s.toggleIsAvailable)
s.ConnectRemoveAccount(s.removeAccount)
}
// get is a getter for account info pointer.
func (s *AccountsModel) get(index int) *AccountInfo {
if index < 0 || index >= len(s.Accounts()) {
return NewAccountInfo(nil)
} else {
return s.Accounts()[index]
}
}
// data is a getter for account info data.
func (s *AccountsModel) data(index *core.QModelIndex, role int) *core.QVariant {
if !index.IsValid() {
return core.NewQVariant()
}
if index.Row() >= len(s.Accounts()) {
return core.NewQVariant()
}
var p = s.Accounts()[index.Row()]
switch role {
case Account:
return NewQVariantString(p.Account())
case UserID:
return NewQVariantString(p.UserID())
case Status:
return NewQVariantString(p.Status())
case Hostname:
return NewQVariantString(p.Hostname())
case Password:
return NewQVariantString(p.Password())
case Security:
return NewQVariantString(p.Security())
case PortIMAP:
return NewQVariantInt(p.PortIMAP())
case PortSMTP:
return NewQVariantInt(p.PortSMTP())
case Aliases:
return NewQVariantString(p.Aliases())
case IsExpanded:
return NewQVariantBool(p.IsExpanded())
case IsCombinedAddressMode:
return NewQVariantBool(p.IsCombinedAddressMode())
default:
return core.NewQVariant()
}
}
// rowCount returns the dimension of model: number of rows is equivalent to number of items in list.
func (s *AccountsModel) rowCount(parent *core.QModelIndex) int {
return len(s.Accounts())
}
// columnCount returns the dimension of model: AccountsModel has only one column.
func (s *AccountsModel) columnCount(parent *core.QModelIndex) int {
return 1
}
// roleNames returns the names of available item properties.
func (s *AccountsModel) roleNames() map[int]*core.QByteArray {
return s.Roles()
}
// addAccount is connected to the addAccount slot.
func (s *AccountsModel) addAccount(p *AccountInfo) {
s.BeginInsertRows(core.NewQModelIndex(), len(s.Accounts()), len(s.Accounts()))
s.SetAccounts(append(s.Accounts(), p))
s.SetCount(len(s.Accounts()))
s.EndInsertRows()
}
// Method connected to toggleIsAvailable slot.
func (s *AccountsModel) toggleIsAvailable(row int) {
var p = s.Accounts()[row]
currentStatus := p.Status()
if currentStatus == "active" {
p.SetStatus("disabled")
} else if currentStatus == "disabled" {
p.SetStatus("active")
} else {
p.SetStatus("error")
}
var pIndex = s.Index(row, 0, core.NewQModelIndex())
s.DataChanged(pIndex, pIndex, []int{Status})
}
// Method connected to removeAccount slot.
func (s *AccountsModel) removeAccount(row int) {
s.BeginRemoveRows(core.NewQModelIndex(), row, row)
s.SetAccounts(append(s.Accounts()[:row], s.Accounts()[row+1:]...))
s.SetCount(len(s.Accounts()))
s.EndRemoveRows()
}
// Remove all items in model.
func (s *AccountsModel) Clear() {
s.BeginRemoveRows(core.NewQModelIndex(), 0, len(s.Accounts()))
s.SetAccounts(s.Accounts()[0:0])
s.SetCount(len(s.Accounts()))
s.EndRemoveRows()
}
// Print the content of account models to console.
func (s *AccountsModel) Dump() {
fmt.Printf("Dimensions rows %d cols %d\n", s.rowCount(nil), s.columnCount(nil))
for iAcc := 0; iAcc < s.rowCount(nil); iAcc++ {
var p = s.Accounts()[iAcc]
fmt.Printf(" %d. %s\n", iAcc, p.Account())
}
}

View File

@ -0,0 +1,213 @@
// Copyright (c) 2020 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/>.
// +build !nogui
package qt
import (
"fmt"
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"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"
)
func (s *FrontendQt) loadAccounts() {
accountMutex.Lock()
defer accountMutex.Unlock()
// Update users.
s.Accounts.Clear()
users := s.bridge.GetUsers()
// If there are no active accounts.
if len(users) == 0 {
log.Info("No active accounts")
return
}
for _, user := range users {
acc_info := NewAccountInfo(nil)
username := user.Username()
if username == "" {
username = user.ID()
}
acc_info.SetAccount(username)
// Set status.
if user.IsConnected() {
acc_info.SetStatus("connected")
} else {
acc_info.SetStatus("disconnected")
}
// Set login info.
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))
// Set aliases.
acc_info.SetAliases(strings.Join(user.GetAddresses(), ";"))
acc_info.SetIsExpanded(user.ID() == s.userIDAdded)
acc_info.SetIsCombinedAddressMode(user.IsCombinedAddressMode())
s.Accounts.addAccount(acc_info)
}
// Updated can clear.
s.userIDAdded = ""
}
func (s *FrontendQt) clearCache() {
defer s.Qml.ProcessFinished()
if err := s.bridge.ClearData(); err != nil {
log.Error("While clearing cache: ", err)
}
// Clearing data removes everything (db, preferences, ...)
// so everything has to be stopped and started again.
s.Qml.SetIsRestarting(true)
s.App.Quit()
}
func (s *FrontendQt) clearKeychain() {
defer s.Qml.ProcessFinished()
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.
s.Qml.NotifyHasNoKeychain()
}
}
}
}
func (s *FrontendQt) logoutAccount(iAccount int) {
defer s.Qml.ProcessFinished()
userID := s.Accounts.get(iAccount).UserID()
user, err := s.bridge.GetUser(userID)
if err != nil {
log.Error("While logging out ", userID, ": ", err)
return
}
if err := user.Logout(); err != nil {
log.Error("While logging out ", userID, ": ", err)
}
}
func (s *FrontendQt) showLoginError(err error, scope string) bool {
if err == nil {
s.Qml.SetConnectionStatus(true) // If we are here connection is ok.
return false
}
log.Warnf("%s: %v", scope, err)
if err == pmapi.ErrAPINotReachable {
s.Qml.SetConnectionStatus(false)
s.SendNotification(TabAccount, s.Qml.CanNotReachAPI())
s.Qml.ProcessFinished()
return true
}
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)
return true
}
// login returns:
// -1: when error occurred
// 0: when no 2FA and no MBOX
// 1: when has 2FA
// 2: when has no 2FA but have MBOX
func (s *FrontendQt) login(login, password string) int {
var err error
s.authClient, s.auth, err = s.bridge.Login(login, password)
if s.showLoginError(err, "login") {
return -1
}
if s.auth.HasTwoFactor() {
return 1
}
if s.auth.HasMailboxPassword() {
return 2
}
return 0 // No 2FA, no mailbox password.
}
// auth2FA returns:
// -1 : error (use SetAddAccountWarning to show message)
// 0 : single password mode
// 1 : two password mode
func (s *FrontendQt) auth2FA(twoFacAuth string) int {
var err error
if s.auth == nil || s.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient)
} else {
_, err = s.authClient.Auth2FA(twoFacAuth, s.auth)
}
if s.showLoginError(err, "auth2FA") {
return -1
}
if s.auth.HasMailboxPassword() {
return 1 // Ask for mailbox password.
}
return 0 // One password.
}
// addAccount adds an account. It should close login modal ProcessFinished if ok.
func (s *FrontendQt) addAccount(mailboxPassword string) int {
if s.auth == nil || s.authClient == nil {
log.Errorf("Missing authentication in addAccount %p %p", s.auth, s.authClient)
s.Qml.SetAddAccountWarning(s.Qml.WrongMailboxPassword(), -2)
return -1
}
user, err := s.bridge.FinishLogin(s.authClient, s.auth, mailboxPassword)
if err != nil {
log.WithError(err).Error("Login was unsuccessful")
s.Qml.SetAddAccountWarning("Failure: "+err.Error(), -2)
return -1
}
s.userIDAdded = user.ID()
s.eventListener.Emit(events.UserRefreshEvent, user.ID())
s.Qml.ProcessFinished()
return 0
}
func (s *FrontendQt) deleteAccount(iAccount int, removePreferences bool) {
defer s.Qml.ProcessFinished()
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 {
s.Qml.NotifyHasNoKeychain()
return
}
s.SendNotification(TabSettings, err.Error())
return
}
}

View File

@ -0,0 +1,645 @@
// Copyright (c) 2020 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/>.
// +build !nogui
// Package qt is the Qt User interface for Desktop bridge.
//
// The FrontendQt implements Frontend interface: `frontend.go`.
// The helper functions are in `helpers.go`.
// Notification specific is written in `notification.go`.
// The AccountsModel is container providing account info to QML ListView.
//
// Since we are using QML there is only one Qt loop in `ui.go`.
package qt
import (
"errors"
"os"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
"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/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/ProtonMail/go-autostart"
//"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/kardianos/osext"
"github.com/skratchdot/open-golang/open"
"github.com/therecipe/qt/core"
"github.com/therecipe/qt/gui"
"github.com/therecipe/qt/qml"
"github.com/therecipe/qt/widgets"
)
var log = config.GetLogEntry("frontend-qt")
var accountMutex = &sync.Mutex{}
// API between Bridge and Qt.
//
// With this interface it is possible to control Qt-Gui interface using pointers to
// Qt and QML objects. QML signals and slots are connected via methods of GoQMLInterface.
type FrontendQt struct {
version string
buildVersion string
showWindowOnStart bool
panicHandler types.PanicHandler
config *config.Config
preferences *config.Preferences
eventListener listener.Listener
updates 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).
authClient bridge.PMAPIProvider
auth *pmapi.Auth
AutostartEntry *autostart.App
// expand userID when added
userIDAdded string
notifyHasNoKeychain bool
}
// New returns a new Qt frontendend for the bridge.
func New(
version,
buildVersion string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
eventListener listener.Listener,
updates types.Updater,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
) *FrontendQt {
prgName := "ProtonMail Bridge"
tmp := &FrontendQt{
version: version,
buildVersion: buildVersion,
showWindowOnStart: showWindowOnStart,
panicHandler: panicHandler,
config: config,
preferences: preferences,
eventListener: eventListener,
updates: updates,
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)
}
// Nicer string for OS.
currentOS := core.QSysInfo_PrettyProductName()
bridge.SetCurrentOS(currentOS)
return tmp
}
// InstanceExistAlert is a global warning window indicating an instance already exists.
func (s *FrontendQt) InstanceExistAlert() {
log.Warn("Instance already exists")
s.QtSetupCoreAndControls()
s.App = widgets.NewQApplication(len(os.Args), os.Args)
s.View = qml.NewQQmlApplicationEngine(s.App)
s.View.AddImportPath("qrc:///")
s.View.Load(core.NewQUrl3("qrc:/BridgeUI/InstanceExistsWindow.qml", 0))
_ = gui.QGuiApplication_Exec()
}
// 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
}
go func() {
defer s.panicHandler.HandlePanic()
s.watchEvents()
}()
err = s.qtExecute(func(s *FrontendQt) error { return nil })
return err
}
func (s *FrontendQt) watchEvents() {
errorCh := s.getEventChannel(events.ErrorEvent)
outgoingNoEncCh := s.getEventChannel(events.OutgoingNoEncEvent)
noActiveKeyForRecipientCh := s.getEventChannel(events.NoActiveKeyForRecipientEvent)
internetOffCh := s.getEventChannel(events.InternetOffEvent)
internetOnCh := s.getEventChannel(events.InternetOnEvent)
secondInstanceCh := s.getEventChannel(events.SecondInstanceEvent)
restartBridgeCh := s.getEventChannel(events.RestartBridgeEvent)
addressChangedCh := s.getEventChannel(events.AddressChangedEvent)
addressChangedLogoutCh := s.getEventChannel(events.AddressChangedLogoutEvent)
logoutCh := s.getEventChannel(events.LogoutEvent)
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent)
newUserCh := s.getEventChannel(events.UserRefreshEvent)
certIssue := s.getEventChannel(events.TLSCertIssue)
for {
select {
case errorDetails := <-errorCh:
imapIssue := strings.Contains(errorDetails, "IMAP failed")
smtpIssue := strings.Contains(errorDetails, "SMTP failed")
s.Qml.NotifyPortIssue(imapIssue, smtpIssue)
case idAndSubject := <-outgoingNoEncCh:
idAndSubjectSlice := strings.SplitN(idAndSubject, ":", 2)
messageID := idAndSubjectSlice[0]
subject := idAndSubjectSlice[1]
s.Qml.ShowOutgoingNoEncPopup(messageID, subject)
case email := <-noActiveKeyForRecipientCh:
s.Qml.ShowNoActiveKeyForRecipient(email)
case <-internetOffCh:
s.Qml.SetConnectionStatus(false)
case <-internetOnCh:
s.Qml.SetConnectionStatus(true)
case <-secondInstanceCh:
s.Qml.ShowWindow()
case <-restartBridgeCh:
s.Qml.SetIsRestarting(true)
s.App.Quit()
case address := <-addressChangedCh:
s.Qml.NotifyAddressChanged(address)
case address := <-addressChangedLogoutCh:
s.Qml.NotifyAddressChangedLogout(address)
case userID := <-logoutCh:
user, err := s.bridge.GetUser(userID)
if err != nil {
return
}
s.Qml.NotifyLogout(user.Username())
case <-updateApplicationCh:
s.Qml.ProcessFinished()
s.Qml.NotifyUpdate()
case <-newUserCh:
s.Qml.LoadAccounts()
case <-certIssue:
s.Qml.ShowCertIssue()
}
}
}
func (s *FrontendQt) getEventChannel(event string) <-chan string {
ch := make(chan string)
s.eventListener.Add(event, ch)
return ch
}
// Loop function for tests.
//
// It runs QtExecute in new thread with function returning itself after setup.
// Therefore it is possible to run tests on background.
func (s *FrontendQt) Start() (err error) {
uiready := make(chan *FrontendQt)
go func() {
err := s.qtExecute(func(self *FrontendQt) error {
// NOTE: Trick to send back UI by channel to access functionality
// inside application thread. Other only uninitialized `ui` is visible.
uiready <- self
return nil
})
if err != nil {
log.Error(err)
}
uiready <- nil
}()
// Receive UI pointer and set all pointers.
running := <-uiready
s.App = running.App
s.View = running.View
s.MainWin = running.MainWin
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 {
arg := core.NewQGenericArgument("", nil)
PauseLong()
isGoodMethod := core.QMetaObject_InvokeMethod4(s.MainWin, method, arg, arg, arg, arg, arg, arg, arg, arg, arg, arg)
if isGoodMethod == false {
return errors.New("Wrong method " + method)
}
return nil
}
// QtSetupCoreAndControls hanldes global setup of Qt.
// Should be called once per program. Probably once per thread is fine.
func (s *FrontendQt) QtSetupCoreAndControls() {
installMessageHandler()
// Core setup.
core.QCoreApplication_SetApplicationName(s.programName)
core.QCoreApplication_SetApplicationVersion(s.programVer)
// High DPI scaling for windows.
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false)
// Software OpenGL: to avoid dedicated GPU.
core.QCoreApplication_SetAttribute(core.Qt__AA_UseSoftwareOpenGL, true)
// Basic style for QuickControls2 objects.
//quickcontrols2.QQuickStyle_SetStyle("material")
}
// qtExecute is the main function for starting the Qt application.
//
// It is better to have just one Qt application per program (at least per same
// thread). This functions reads the main user interface defined in QML files.
// The files are appended to library by Qt-QRC.
func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
s.QtSetupCoreAndControls()
s.App = widgets.NewQApplication(len(os.Args), os.Args)
if runtime.GOOS == "linux" { // Fix default font.
s.App.SetFont(gui.NewQFont2(FcMatchSans(), 12, int(gui.QFont__Normal), false), "")
}
s.App.SetQuitOnLastWindowClosed(false) // Just to make sure it's not closed.
s.View = qml.NewQQmlApplicationEngine(s.App)
// Add Go-QML bridge.
s.Qml = NewGoQMLInterface(nil)
s.Qml.SetIsShownOnStart(s.showWindowOnStart)
s.Qml.SetFrontend(s) // provides access
s.View.RootContext().SetContextProperty("go", s.Qml)
// Set first start flag.
s.Qml.SetIsFirstStart(s.preferences.GetBool(preferences.FirstStartKey))
// Don't repeat next start.
s.preferences.SetBool(preferences.FirstStartKey, false)
// Check if it is first start after update (fresh version).
lastVersion := s.preferences.Get(preferences.LastVersionKey)
s.Qml.SetIsFreshVersion(lastVersion != "" && s.version != lastVersion)
s.preferences.Set(preferences.LastVersionKey, s.version)
// Add AccountsModel.
s.Accounts = NewAccountsModel(nil)
s.View.RootContext().SetContextProperty("accountsModel", s.Accounts)
// Import path and load QML files.
s.View.AddImportPath("qrc:///")
s.View.Load(core.NewQUrl3("qrc:/ui.qml", 0))
// List of used packages.
s.Qml.SetCredits(bridge.Credits)
s.Qml.SetFullversion(s.buildVersion)
// Autostart.
if s.Qml.IsFirstStart() {
if s.AutostartEntry.IsEnabled() {
if err := s.AutostartEntry.Disable(); err != nil {
log.Error("First disable ", err)
s.autostartError(err)
}
}
s.toggleAutoStart()
}
if s.AutostartEntry.IsEnabled() {
s.Qml.SetIsAutoStart(true)
} else {
s.Qml.SetIsAutoStart(false)
}
if s.preferences.GetBool(preferences.AllowProxyKey) {
s.Qml.SetIsProxyAllowed(true)
} else {
s.Qml.SetIsProxyAllowed(false)
}
// Notify user about error during initialization.
if s.notifyHasNoKeychain {
s.Qml.NotifyHasNoKeychain()
}
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))
// IMAP/SMTP ports.
s.Qml.SetIsDefaultPort(
s.config.GetDefaultIMAPPort() == s.preferences.GetInt(preferences.IMAPPortKey) &&
s.config.GetDefaultSMTPPort() == s.preferences.GetInt(preferences.SMTPPortKey),
)
// Check QML is loaded properly.
if len(s.View.RootObjects()) == 0 {
return errors.New("QML not loaded properly")
}
// Obtain main window (need for invoke method).
s.MainWin = s.View.RootObjects()[0]
SetupSystray(s)
// Injected procedure for out-of-main-thread applications.
if err := Procedure(s); err != nil {
return err
}
// Loop
if ret := gui.QGuiApplication_Exec(); ret != 0 {
err := errors.New("Event loop ended with return value:" + string(ret))
log.Warn("QGuiApplication_Exec: ", err)
return err
}
HideSystray()
return nil
}
func (s *FrontendQt) openLogs() {
go open.Run(s.config.GetLogDir())
}
// Check version in separate goroutine to not block the GUI (avoid program not responding message).
func (s *FrontendQt) isNewVersionAvailable(showMessage bool) {
go func() {
defer s.panicHandler.HandlePanic()
defer s.Qml.ProcessFinished()
isUpToDate, latestVersionInfo, err := s.updates.CheckIsBridgeUpToDate()
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")
}()
}
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)
}
func (s *FrontendQt) sendBug(description, client, address string) (isOK bool) {
isOK = true
var accname = "No account logged in"
if s.Accounts.Count() > 0 {
accname = s.Accounts.get(0).Account()
}
if err := s.bridge.ReportBug(
core.QSysInfo_ProductType(),
core.QSysInfo_PrettyProductName(),
description,
accname,
address,
client,
); err != nil {
log.Error("while sendBug: ", err)
isOK = false
}
return
}
func (s *FrontendQt) getLastMailClient() string {
return s.bridge.GetCurrentClient()
}
func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
acc := s.Accounts.get(iAccount)
user, err := s.bridge.GetUser(acc.UserID())
if err != nil {
log.Warn("UserConfigFromKeychain failed: ", acc.Account(), err)
s.SendNotification(TabAccount, s.Qml.GenericErrSeeLogs())
return
}
imapPort := s.preferences.GetInt(preferences.IMAPPortKey)
imapSSL := false
smtpPort := s.preferences.GetInt(preferences.SMTPPortKey)
smtpSSL := s.preferences.GetBool(preferences.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)
log.Warn("Detected Catalina or newer with bad SMTP SSL settings, now using SSL, bridge needs to restart")
doRestart = true
}
for _, autoConf := range autoconfig.Available() {
if err := autoConf.Configure(imapPort, smtpPort, imapSSL, smtpSSL, user, iAddress); err != nil {
log.Warn("Autoconfig failed: ", autoConf.Name(), err)
s.SendNotification(TabAccount, s.Qml.GenericErrSeeLogs())
return
}
}
if doRestart {
time.Sleep(2 * time.Second)
s.Qml.SetIsRestarting(true)
s.App.Quit()
}
return
}
func (s *FrontendQt) toggleAutoStart() {
defer s.Qml.ProcessFinished()
var err error
if s.AutostartEntry.IsEnabled() {
err = s.AutostartEntry.Disable()
} else {
err = s.AutostartEntry.Enable()
}
if err != nil {
log.Error("Enable autostart: ", err)
s.autostartError(err)
}
if s.AutostartEntry.IsEnabled() {
s.Qml.SetIsAutoStart(true)
} else {
s.Qml.SetIsAutoStart(false)
}
}
func (s *FrontendQt) toggleAllowProxy() {
defer s.Qml.ProcessFinished()
if s.preferences.GetBool(preferences.AllowProxyKey) {
s.preferences.SetBool(preferences.AllowProxyKey, false)
bridge.DisallowDoH()
s.Qml.SetIsProxyAllowed(false)
} else {
s.preferences.SetBool(preferences.AllowProxyKey, true)
bridge.AllowDoH()
s.Qml.SetIsProxyAllowed(true)
}
}
func (s *FrontendQt) getIMAPPort() string {
return s.preferences.Get(preferences.IMAPPortKey)
}
func (s *FrontendQt) getSMTPPort() string {
return s.preferences.Get(preferences.SMTPPortKey)
}
// Return 0 -- port is free to use for server.
// Return 1 -- port is occupied.
func (s *FrontendQt) isPortOpen(portStr string) int {
portInt, err := strconv.Atoi(portStr)
if err != nil {
return 1
}
if !ports.IsPortFree(portInt) {
return 1
}
return 0
}
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)
}
func (s *FrontendQt) isSMTPSTARTTLS() bool {
return !s.preferences.GetBool(preferences.SMTPSSLKey)
}
func (s *FrontendQt) checkInternet() {
s.Qml.SetConnectionStatus(IsInternetAvailable())
}
func (s *FrontendQt) switchAddressModeUser(iAccount int) {
defer s.Qml.ProcessFinished()
userID := s.Accounts.get(iAccount).UserID()
user, err := s.bridge.GetUser(userID)
if err != nil {
log.Error("Get user for switch address mode failed: ", err)
s.SendNotification(TabAccount, s.Qml.GenericErrSeeLogs())
return
}
if err := user.SwitchAddressMode(); err != nil {
log.Error("Switch address mode failed: ", err)
s.SendNotification(TabAccount, s.Qml.GenericErrSeeLogs())
return
}
s.userIDAdded = userID
}
func (s *FrontendQt) autostartError(err error) {
if strings.Contains(err.Error(), "permission denied") {
s.Qml.FailedAutostartCode("permission")
} else if strings.Contains(err.Error(), "error code: 0x") {
errorCode := err.Error()
errorCode = errorCode[len(errorCode)-8:]
s.Qml.FailedAutostartCode(errorCode)
} else {
s.Qml.FailedAutostartCode("")
}
}
func (s *FrontendQt) toggleIsReportingOutgoingNoEnc() {
shouldReport := !s.Qml.IsReportingOutgoingNoEnc()
s.preferences.SetBool(preferences.ReportOutgoingNoEncKey, shouldReport)
s.Qml.SetIsReportingOutgoingNoEnc(shouldReport)
}
func (s *FrontendQt) shouldSendAnswer(messageID string, shouldSend bool) {
s.noEncConfirmator.ConfirmNoEncryption(messageID, shouldSend)
}
func (s *FrontendQt) saveOutgoingNoEncPopupCoord(x, y float32) {
//prefs.SetFloat(prefs.OutgoingNoEncPopupCoordX, x)
//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(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
}
}
}()
go func() {
defer s.panicHandler.HandlePanic()
s.updates.StartUpgrade(progress)
}()
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2020 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/>.
// +build nogui
package qt
import (
"fmt"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
var log = config.GetLogEntry("frontend-nogui") //nolint[gochecknoglobals]
type FrontendHeadless struct{}
func (s *FrontendHeadless) Loop(credentialsError error) error {
log.Info("Check status on localhost:8081")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Bridge is running")
})
return http.ListenAndServe(":8081", nil)
}
func (s *FrontendHeadless) InstanceExistAlert() {}
func (s *FrontendHeadless) IsAppRestarting() bool { return false }
func New(
version,
buildVersion string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
eventListener listener.Listener,
updates types.Updater,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
) *FrontendHeadless {
return &FrontendHeadless{}
}

View File

@ -0,0 +1,84 @@
// Copyright (c) 2020 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/>.
// +build !nogui
package qt
import (
"bufio"
"os"
"os/exec"
"time"
"github.com/ProtonMail/proton-bridge/pkg/connection"
"github.com/therecipe/qt/core"
)
// NewQByteArrayFromString is a wrapper for new QByteArray from string.
func NewQByteArrayFromString(name string) *core.QByteArray {
return core.NewQByteArray2(name, len(name))
}
// NewQVariantString is a wrapper for QVariant alocator String.
func NewQVariantString(data string) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantStringArray is a wrapper for QVariant alocator String Array.
func NewQVariantStringArray(data []string) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantBool is a wrapper for QVariant alocator Bool.
func NewQVariantBool(data bool) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantInt is a wrapper for QVariant alocator Int.
func NewQVariantInt(data int) *core.QVariant {
return core.NewQVariant1(data)
}
// Pause is used to show GUI tests.
func Pause() {
time.Sleep(500 * time.Millisecond)
}
// PauseLong is used to diplay GUI tests.
func PauseLong() {
time.Sleep(3 * time.Second)
}
func IsInternetAvailable() bool {
return connection.CheckInternetConnection() == nil
}
// FIXME: Not working in test...
func WaitForEnter() {
log.Print("Press 'Enter' to continue...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
}
func FcMatchSans() (family string) {
family = "DejaVu Sans"
fcMatch, err := exec.Command("fc-match", "-f", "'%{family}'", "sans-serif").Output()
if err == nil {
return string(fcMatch)
}
return
}

View File

@ -0,0 +1,23 @@
// +build !nogui
#include "logs.h"
#include "_cgo_export.h"
#include <QByteArray>
#include <QString>
#include <QtGlobal>
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
Q_UNUSED(type);
Q_UNUSED(context);
QByteArray localMsg = msg.toUtf8().prepend("WHITESPACE");
logMsgPacked(
const_cast<char*>( (localMsg.constData()) +10 ),
localMsg.size()-10
);
//printf("Handler: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
}
void InstallMessageHandler() { qInstallMessageHandler(messageHandler); }

View File

@ -0,0 +1,38 @@
// Copyright (c) 2020 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/>.
// +build !nogui
package qt
//#include "logs.h"
import "C"
import (
"github.com/sirupsen/logrus"
)
func installMessageHandler() {
C.InstallMessageHandler()
}
//export logMsgPacked
func logMsgPacked(data *C.char, len C.int) {
log.WithFields(logrus.Fields{
"pkg": "frontend-qml",
}).Warnln(C.GoStringN(data, len))
}

View File

@ -0,0 +1,20 @@
#pragma once
#ifndef GO_LOG_H
#define GO_LOG_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif // C++
void InstallMessageHandler();
;
#ifdef __cplusplus
}
#endif // C++
#endif // LOG

View File

@ -0,0 +1,33 @@
// Copyright (c) 2020 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/>.
// +build !nogui
package qt
const (
TabAccount = 0
TabSettings = 1
TabHelp = 2
TabQuit = 4
TabUpdates = 100
TabAddAccount = -1
)
func (s *FrontendQt) SendNotification(tabIndex int, msg string) {
s.Qml.NotifyBubble(tabIndex, msg)
}

View File

@ -0,0 +1,77 @@
<!--This file is process during qtdeploy and resources are added to executable.-->
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="ProtonUI">
<file alias="qmldir" >../qml/ProtonUI/qmldir</file>
<file alias="AccessibleButton.qml" >../qml/ProtonUI/AccessibleButton.qml</file>
<file alias="AccessibleText.qml" >../qml/ProtonUI/AccessibleText.qml</file>
<file alias="AccessibleSelectableText.qml" >../qml/ProtonUI/AccessibleSelectableText.qml</file>
<file alias="AccountView.qml" >../qml/ProtonUI/AccountView.qml</file>
<file alias="AddAccountBar.qml" >../qml/ProtonUI/AddAccountBar.qml</file>
<file alias="BubbleNote.qml" >../qml/ProtonUI/BubbleNote.qml</file>
<file alias="BugReportWindow.qml" >../qml/ProtonUI/BugReportWindow.qml</file>
<file alias="ButtonIconText.qml" >../qml/ProtonUI/ButtonIconText.qml</file>
<file alias="ButtonRounded.qml" >../qml/ProtonUI/ButtonRounded.qml</file>
<file alias="CheckBoxLabel.qml" >../qml/ProtonUI/CheckBoxLabel.qml</file>
<file alias="ClickIconText.qml" >../qml/ProtonUI/ClickIconText.qml</file>
<file alias="Dialog.qml" >../qml/ProtonUI/Dialog.qml</file>
<file alias="DialogAddUser.qml" >../qml/ProtonUI/DialogAddUser.qml</file>
<file alias="DialogUpdate.qml" >../qml/ProtonUI/DialogUpdate.qml</file>
<file alias="DialogConnectionTroubleshoot.qml" >../qml/ProtonUI/DialogConnectionTroubleshoot.qml</file>
<file alias="FileAndFolderSelect.qml" >../qml/ProtonUI/FileAndFolderSelect.qml</file>
<file alias="InformationBar.qml" >../qml/ProtonUI/InformationBar.qml</file>
<file alias="InputField.qml" >../qml/ProtonUI/InputField.qml</file>
<file alias="InstanceExistsWindow.qml" >../qml/ProtonUI/InstanceExistsWindow.qml</file>
<file alias="LogoHeader.qml" >../qml/ProtonUI/LogoHeader.qml</file>
<file alias="PopupMessage.qml" >../qml/ProtonUI/PopupMessage.qml</file>
<file alias="Style.qml" >../qml/ProtonUI/Style.qml</file>
<file alias="TabButton.qml" >../qml/ProtonUI/TabButton.qml</file>
<file alias="TabLabels.qml" >../qml/ProtonUI/TabLabels.qml</file>
<file alias="TextLabel.qml" >../qml/ProtonUI/TextLabel.qml</file>
<file alias="TextValue.qml" >../qml/ProtonUI/TextValue.qml</file>
<file alias="TLSCertPinIssueBar.qml" >../qml/ProtonUI/TLSCertPinIssueBar.qml</file>
<file alias="WindowTitleBar.qml" >../qml/ProtonUI/WindowTitleBar.qml</file>
<file alias="fontawesome.ttf" >../share/fontawesome-webfont.ttf</file>
</qresource>
<qresource prefix="ProtonUI/images">
<file alias="systray.png" >../share/icons/rounded-systray.png</file>
<file alias="systray-warn.png" >../share/icons/rounded-syswarn.png</file>
<file alias="systray-error.png" >../share/icons/rounded-syswarn.png</file>
<file alias="systray-mono.png" >../share/icons/white-systray.png</file>
<file alias="systray-warn-mono.png" >../share/icons/white-syswarn.png</file>
<file alias="systray-error-mono.png">../share/icons/white-syserror.png</file>
<file alias="icon.png" >../share/icons/rounded-app.png</file>
<file alias="pm_logo.png" >../share/icons/pm_logo.png</file>
<file alias="win10_Dash.png" >../share/icons/win10_Dash.png</file>
<file alias="win10_Times.png" >../share/icons/win10_Times.png</file>
<file alias="macos_gray.png" >../share/icons/macos_gray.png</file>
<file alias="macos_red.png" >../share/icons/macos_red.png</file>
<file alias="macos_red_hl.png" >../share/icons/macos_red_hl.png</file>
<file alias="macos_red_dark.png" >../share/icons/macos_red_dark.png</file>
<file alias="macos_yellow.png" >../share/icons/macos_yellow.png</file>
<file alias="macos_yellow_hl.png" >../share/icons/macos_yellow_hl.png</file>
<file alias="macos_yellow_dark.png" >../share/icons/macos_yellow_dark.png</file>
</qresource>
<qresource prefix="BridgeUI">
<file alias="qmldir" >../qml/BridgeUI/qmldir</file>
<file alias="AccountDelegate.qml" >../qml/BridgeUI/AccountDelegate.qml</file>
<file alias="BubbleMenu.qml" >../qml/BridgeUI/BubbleMenu.qml</file>
<file alias="Credits.qml" >../qml/BridgeUI/Credits.qml</file>
<file alias="DialogFirstStart.qml" >../qml/BridgeUI/DialogFirstStart.qml</file>
<file alias="DialogPortChange.qml" >../qml/BridgeUI/DialogPortChange.qml</file>
<file alias="DialogYesNo.qml" >../qml/BridgeUI/DialogYesNo.qml</file>
<file alias="DialogTLSCertInfo.qml" >../qml/BridgeUI/DialogTLSCertInfo.qml</file>
<file alias="HelpView.qml" >../qml/BridgeUI/HelpView.qml</file>
<file alias="InfoWindow.qml" >../qml/BridgeUI/InfoWindow.qml</file>
<file alias="MainWindow.qml" >../qml/BridgeUI/MainWindow.qml</file>
<file alias="ManualWindow.qml" >../qml/BridgeUI/ManualWindow.qml</file>
<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>
<file alias="ui.qml" >../qml/Gui.qml</file>
</qresource>
</RCC>

View File

@ -0,0 +1,117 @@
// Copyright (c) 2020 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/>.
// +build !nogui
package qt
import (
"runtime"
"github.com/therecipe/qt/core"
"github.com/therecipe/qt/gui"
"github.com/therecipe/qt/widgets"
)
const (
systrayNormal = ""
systrayWarning = "-warn"
systrayError = "-error"
)
func min(a, b int) int {
if b < a {
return b
}
return a
}
func max(a, b int) int {
if b > a {
return b
}
return a
}
var systray *widgets.QSystemTrayIcon
func SetupSystray(frontend *FrontendQt) {
systray = widgets.NewQSystemTrayIcon(nil)
NormalSystray()
systray.SetToolTip(frontend.programName)
systray.SetContextMenu(createMenu(frontend, systray))
if runtime.GOOS != "darwin" {
systray.ConnectActivated(func(reason widgets.QSystemTrayIcon__ActivationReason) {
switch reason {
case widgets.QSystemTrayIcon__Trigger, widgets.QSystemTrayIcon__DoubleClick:
frontend.Qml.ShowWindow()
default:
systray.ContextMenu().Exec2(menuPosition(systray), nil)
}
})
}
systray.Show()
}
func qsTr(msg string) string {
return systray.Tr(msg, "Systray menu", -1)
}
func createMenu(frontend *FrontendQt, systray *widgets.QSystemTrayIcon) *widgets.QMenu {
menu := widgets.NewQMenu(nil)
menu.AddAction(qsTr("Open")).ConnectTriggered(func(ok bool) { frontend.Qml.ShowWindow() })
menu.AddAction(qsTr("Help")).ConnectTriggered(func(ok bool) { frontend.Qml.ShowHelp() })
menu.AddAction(qsTr("Quit")).ConnectTriggered(func(ok bool) { frontend.Qml.ShowQuit() })
return menu
}
func menuPosition(systray *widgets.QSystemTrayIcon) *core.QPoint {
var availRec = gui.QGuiApplication_PrimaryScreen().AvailableGeometry()
var trayRec = systray.Geometry()
var x = max(availRec.Left(), min(trayRec.X(), availRec.Right()-trayRec.Width()))
var y = max(availRec.Top(), min(trayRec.Y(), availRec.Bottom()-trayRec.Height()))
return core.NewQPoint2(x, y)
}
func showSystray(systrayType string) {
path := ":/ProtonUI/images/systray" + systrayType
if runtime.GOOS == "darwin" {
path += "-mono"
}
path += ".png"
icon := gui.NewQIcon5(path)
icon.SetIsMask(true)
systray.SetIcon(icon)
}
func NormalSystray() {
showSystray(systrayNormal)
}
func HighlightSystray() {
showSystray(systrayWarning)
}
func ErrorSystray() {
showSystray(systrayError)
}
func HideSystray() {
systray.Hide()
}

View File

@ -0,0 +1,669 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>AccountDelegate</name>
<message>
<location filename="qml/BridgeUI/AccountDelegate.qml" line="107"/>
<source>Logout</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AccountDelegate.qml" line="121"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AccountDelegate.qml" line="284"/>
<source>connected</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AccountDelegate.qml" line="289"/>
<source>Log out</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AccountDelegate.qml" line="316"/>
<location filename="qml/BridgeUI/AccountDelegate.qml" line="346"/>
<source>disconnected</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AccountDelegate.qml" line="321"/>
<location filename="qml/BridgeUI/AccountDelegate.qml" line="351"/>
<source>Log in</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AccountView</name>
<message>
<location filename="qml/BridgeUI/AccountView.qml" line="56"/>
<source>No accounts added</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AccountView.qml" line="67"/>
<source>ACCOUNT</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AccountView.qml" line="78"/>
<source>STATUS</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AccountView.qml" line="89"/>
<source>ACTIONS</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AddAccountBar</name>
<message>
<location filename="qml/BridgeUI/AddAccountBar.qml" line="20"/>
<source>Add Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/AddAccountBar.qml" line="33"/>
<source>Help</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>BubbleMenu</name>
<message>
<location filename="qml/BridgeUI/BubbleMenu.qml" line="40"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>BugReportWindow</name>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="44"/>
<source>Please write a brief description of the bug(s) you have encountered...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="67"/>
<source>Email client:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="99"/>
<source>Contact email:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="135"/>
<source>Bug reports are not end-to-end encrypted!</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="136"/>
<source>Please do not send any sensitive information.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="137"/>
<source>Contact us at security@protonmail.com for critical security issues.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="147"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="158"/>
<source>Send</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/BugReportWindow.qml" line="173"/>
<source>Field required</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DialogAddUser</name>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="10"/>
<source>Log in to your ProtonMail account</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="53"/>
<source>Username:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="65"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="71"/>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="146"/>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="196"/>
<source>Next</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="91"/>
<source>Sign Up for an Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="111"/>
<source>Password for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="176"/>
<source>Mailbox password for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="127"/>
<source>Two Factor Code</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="140"/>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="190"/>
<source>Back</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="163"/>
<source>Logging in</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="218"/>
<source>Adding account, please wait ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogAddUser.qml" line="279"/>
<source>Required field</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DialogPortChange</name>
<message>
<location filename="qml/BridgeUI/DialogPortChange.qml" line="20"/>
<source>IMAP port</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogPortChange.qml" line="29"/>
<source>SMTP port</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogPortChange.qml" line="42"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogPortChange.qml" line="50"/>
<source>Okay</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogPortChange.qml" line="68"/>
<source>Settings will be applied after next start. You may need to re-configure your email client.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogPortChange.qml" line="70"/>
<source>Bridge will now restart.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DialogYesNo</name>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="68"/>
<source>Additionally delete all stored preferences and data</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="91"/>
<source>No</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="99"/>
<source>Yes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="119"/>
<source>Waiting...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="131"/>
<source>Close Bridge</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="132"/>
<source>Are you sure you want to close the Bridge?</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="133"/>
<source>Closing Bridge...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="141"/>
<source>Logout</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="143"/>
<source>Logging out...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="151"/>
<source>Delete account</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="152"/>
<source>Are you sure you want to remove this account?</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="153"/>
<source>Deleting ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="161"/>
<source>Clear keychain</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="162"/>
<source>Are you sure you want to clear your keychain?</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="163"/>
<source>Clearing the keychain ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="171"/>
<source>Clear cache</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="172"/>
<source>Are you sure you want to clear your local cache?</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="173"/>
<source>Clearing the cache ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="183"/>
<source>Checking for updates ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="194"/>
<source>Turning on automatic start of Bridge...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="195"/>
<source>Turning off automatic start of Bridge...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/DialogYesNo.qml" line="252"/>
<source>You have the latest version!</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Gui</name>
<message>
<location filename="qml/Gui.qml" line="95"/>
<source>Account %1 has been disconnected. Please log in to continue to use the Bridge with this account.</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<location filename="qml/Gui.qml" line="118"/>
<source>Incorrect username or password.</source>
<comment>notification</comment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="qml/Gui.qml" line="119"/>
<source>Incorrect mailbox password.</source>
<comment>notification</comment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="qml/Gui.qml" line="120"/>
<source>Cannot contact server, please check your internet connection.</source>
<comment>notification</comment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="qml/Gui.qml" line="121"/>
<source>Credentials could not be removed.</source>
<comment>notification</comment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="qml/Gui.qml" line="122"/>
<source>Unable to submit bug report.</source>
<comment>notification</comment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="qml/Gui.qml" line="123"/>
<source>Bug report successfully sent.</source>
<comment>notification</comment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>HelpView</name>
<message>
<location filename="qml/BridgeUI/HelpView.qml" line="24"/>
<source>Logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelpView.qml" line="34"/>
<source>Report Bug</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelpView.qml" line="44"/>
<source>Setup Guide</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelpView.qml" line="54"/>
<source>Check for Updates</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelpView.qml" line="83"/>
<source>Credits</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>HelperPane</name>
<message>
<location filename="qml/BridgeUI/HelperPane.qml" line="37"/>
<source>Skip</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelperPane.qml" line="50"/>
<source>Back</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelperPane.qml" line="60"/>
<source>Next</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelperPane.qml" line="70"/>
<source>You can add, delete, logout account. Expand account to see settings.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelperPane.qml" line="73"/>
<source>This is settings windows: Clear cache, list logs, ... </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/HelperPane.qml" line="76"/>
<source>Welcome to ProtonMail Bridge! Add account to start.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>InfoWindow</name>
<message>
<location filename="qml/BridgeUI/InfoWindow.qml" line="38"/>
<source>IMAP SETTINGS</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InfoWindow.qml" line="43"/>
<location filename="qml/BridgeUI/InfoWindow.qml" line="56"/>
<source>Hostname:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InfoWindow.qml" line="44"/>
<location filename="qml/BridgeUI/InfoWindow.qml" line="57"/>
<source>Port:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InfoWindow.qml" line="45"/>
<location filename="qml/BridgeUI/InfoWindow.qml" line="58"/>
<source>Username:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InfoWindow.qml" line="46"/>
<location filename="qml/BridgeUI/InfoWindow.qml" line="59"/>
<source>Password:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InfoWindow.qml" line="51"/>
<source>SMTP SETTINGS</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InfoWindow.qml" line="74"/>
<source>Configure Apple Mail</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>InformationBar</name>
<message>
<location filename="qml/BridgeUI/InformationBar.qml" line="27"/>
<source>An update is available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InformationBar.qml" line="27"/>
<source>No Internet connection</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InformationBar.qml" line="41"/>
<source>Install</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InformationBar.qml" line="41"/>
<source>Retry</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InformationBar.qml" line="62"/>
<source>Dismiss</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>InstanceExistsWindow</name>
<message>
<location filename="qml/BridgeUI/InstanceExistsWindow.qml" line="16"/>
<source>ProtonMail Bridge</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InstanceExistsWindow.qml" line="32"/>
<source>Warning: Instance exists</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InstanceExistsWindow.qml" line="43"/>
<source>An instance of the ProtonMail Bridge is already running.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InstanceExistsWindow.qml" line="44"/>
<source>Please close the existing ProtonMail Bridge process before starting a new one.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InstanceExistsWindow.qml" line="45"/>
<source>This program will close now.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/InstanceExistsWindow.qml" line="54"/>
<source>Okay</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="qml/BridgeUI/MainWindow.qml" line="27"/>
<source>ProtonMail Bridge</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/MainWindow.qml" line="44"/>
<source>Accounts</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/MainWindow.qml" line="45"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/MainWindow.qml" line="46"/>
<source>Help</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/MainWindow.qml" line="117"/>
<source>Click here to start</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/MainWindow.qml" line="241"/>
<source>Credits</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/MainWindow.qml" line="249"/>
<source>Information about version</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SettingsView</name>
<message>
<location filename="qml/BridgeUI/SettingsView.qml" line="23"/>
<source>Clear Cache</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/SettingsView.qml" line="26"/>
<location filename="qml/BridgeUI/SettingsView.qml" line="44"/>
<source>Clear</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/SettingsView.qml" line="41"/>
<source>Clear Keychain</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/SettingsView.qml" line="59"/>
<source>Automatically Start Bridge</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/SettingsView.qml" line="74"/>
<source>Advanced settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/SettingsView.qml" line="89"/>
<source>Change SMTP/IMAP Ports</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/BridgeUI/SettingsView.qml" line="92"/>
<source>Change</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>StatusFooter</name>
<message>
<location filename="qml/BridgeUI/StatusFooter.qml" line="31"/>
<source>Quit</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TabLabels</name>
<message>
<location filename="qml/BridgeUI/TabLabels.qml" line="46"/>
<source>Close Bridge</source>
<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>

192
internal/frontend/qt/ui.go Normal file
View File

@ -0,0 +1,192 @@
// Copyright (c) 2020 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/>.
// +build !nogui
package qt
import (
"runtime"
"github.com/therecipe/qt/core"
)
// Interface between go and qml.
//
// Here we implement all the signals / methods.
type GoQMLInterface struct {
core.QObject
_ func() `constructor:"init"`
_ bool `property:"isAutoStart"`
_ bool `property:"isProxyAllowed"`
_ string `property:"currentAddress"`
_ string `property:"goos"`
_ string `property:"credits"`
_ 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"`
// Translations.
_ string `property:"wrongCredentials"`
_ string `property:"wrongMailboxPassword"`
_ string `property:"canNotReachAPI"`
_ string `property:"credentialsNotRemoved"`
_ string `property:"versionCheckFailed"`
_ string `property:"failedAutostartPerm"`
_ string `property:"failedAutostart"`
_ string `property:"genericErrSeeLogs"`
_ float32 `property:"progress"`
_ int `property:"progressDescription"`
_ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func(updateState string) `signal:"setUpdateState"`
_ func() `slot:"checkInternet"`
_ func(systX, systY, systW, systH int) `signal:"toggleMainWin"`
_ func() `signal:"processFinished"`
_ func() `signal:"openManual"`
_ func(showMessage bool) `signal:"runCheckVersion"`
_ func() `signal:"toggleMainWin"`
_ func() `signal:"showWindow"`
_ func() `signal:"showHelp"`
_ func() `signal:"showQuit"`
_ func() `slot:"toggleAutoStart"`
_ func() `slot:"toggleAllowProxy"`
_ func() `slot:"loadAccounts"`
_ func() `slot:"openLogs"`
_ func() `slot:"clearCache"`
_ func() `slot:"clearKeychain"`
_ func() `slot:"highlightSystray"`
_ func() `slot:"errorSystray"`
_ func() `slot:"normalSystray"`
_ func() `slot:"getLocalVersionInfo"`
_ func(showMessage bool) `slot:"isNewVersionAvailable"`
_ func() string `slot:"getBackendVersion"`
_ func() string `slot:"getIMAPPort"`
_ func() string `slot:"getSMTPPort"`
_ func() string `slot:"getLastMailClient"`
_ func(portStr string) int `slot:"isPortOpen"`
_ func(imapPort, smtpPort string, useSTARTTLSforSMTP bool) `slot:"setPortsAndSecurity"`
_ func() bool `slot:"isSMTPSTARTTLS"`
_ func(description, client, address string) bool `slot:"sendBug"`
_ func(tabIndex int, message string) `signal:"notifyBubble"`
_ func(tabIndex int, message string) `signal:"silentBubble"`
_ func() `signal:"bubbleClosed"`
_ func(iAccount int, removePreferences bool) `slot:"deleteAccount"`
_ func(iAccount int) `slot:"logoutAccount"`
_ func(iAccount int, iAddress int) `slot:"configureAppleMail"`
_ func(iAccount int) `signal:"switchAddressMode"`
_ func(login, password string) int `slot:"login"`
_ func(twoFacAuth string) int `slot:"auth2FA"`
_ func(mailboxPassword string) int `slot:"addAccount"`
_ func(message string, changeIndex int) `signal:"setAddAccountWarning"`
_ 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(busyPortIMAP, busyPortSMTP bool) `signal:"notifyPortIssue"`
_ func(code string) `signal:"failedAutostartCode"`
_ bool `property:"isReportingOutgoingNoEnc"`
_ func() `slot:"toggleIsReportingOutgoingNoEnc"`
_ func(messageID string, shouldSend bool) `slot:"shouldSendAnswer"`
_ func(messageID, subject string) `signal:"showOutgoingNoEncPopup"`
_ func(x, y float32) `signal:"setOutgoingNoEncPopupCoord"`
_ func(x, y float32) `slot:"saveOutgoingNoEncPopupCoord"`
_ func(recipient string) `signal:"showNoActiveKeyForRecipient"`
_ func() `signal:"showCertIssue"`
_ func() `slot:"startUpdate"`
_ func(hasError bool) `signal:"updateFinished"`
}
// init is basically the constructor.
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.ConnectToggleAllowProxy(f.toggleAllowProxy)
s.ConnectLoadAccounts(f.loadAccounts)
s.ConnectOpenLogs(f.openLogs)
s.ConnectClearCache(f.clearCache)
s.ConnectClearKeychain(f.clearKeychain)
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
s.ConnectGetIMAPPort(f.getIMAPPort)
s.ConnectGetSMTPPort(f.getSMTPPort)
s.ConnectGetLastMailClient(f.getLastMailClient)
s.ConnectIsPortOpen(f.isPortOpen)
s.ConnectIsSMTPSTARTTLS(f.isSMTPSTARTTLS)
s.ConnectSendBug(f.sendBug)
s.ConnectDeleteAccount(f.deleteAccount)
s.ConnectLogoutAccount(f.logoutAccount)
s.ConnectConfigureAppleMail(f.configureAppleMail)
s.ConnectLogin(f.login)
s.ConnectAuth2FA(f.auth2FA)
s.ConnectAddAccount(f.addAccount)
s.ConnectSetPortsAndSecurity(f.setPortsAndSecurity)
s.ConnectHighlightSystray(HighlightSystray)
s.ConnectErrorSystray(ErrorSystray)
s.ConnectNormalSystray(NormalSystray)
s.ConnectSwitchAddressMode(f.switchAddressModeUser)
s.SetGoos(runtime.GOOS)
s.SetIsRestarting(false)
s.SetProgramTitle(f.programName)
s.ConnectGetBackendVersion(func() string {
return f.programVer
})
s.ConnectCheckInternet(f.checkInternet)
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)
s.ConnectShouldSendAnswer(f.shouldSendAnswer)
s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord)
s.ConnectStartUpdate(f.StartUpdate)
}

Binary file not shown.

View File

@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "logo.ico"

Some files were not shown because too many files have changed in this diff Show More