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

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)
}