Shared GUI for Bridge and Import/Export

This commit is contained in:
Jakub
2020-05-27 15:58:50 +02:00
committed by Michal Horejsek
parent b598779c0f
commit 49316a935c
96 changed files with 11469 additions and 209 deletions

View File

@ -0,0 +1,60 @@
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")
Icons+= share/images/folder_open.png share/images/envelope_open.png
MocDependencies= ./ui.go ./account_model.go ./folder_structure.go ./folder_functions.go
## EnumDependecies= ../backend/errors/errors.go ../backend/progress.go ../backend/source/enum.go ../frontend/enums.go
all: ../qml/ImportExportUI/images moc.go ../qml/GuiIE.qml qmlcheck rcc.cpp
## ./qml/GuiIE.qml: enums.sh ${EnumDependecies}
## ./enums.sh
../qml/ProtonUI/fontawesome.ttf:
ln -sf ${FontAwesome} $@
../qml/ProtonUI/images:
ln -sf ${ImageDir} $@
../qml/ImportExportUI/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/ImportExportUI/images ../qml/ProtonUI/images
qmlscene -verbose -I ../qml/ -f ../qml/tst_GuiIE.qml --quit
qmlpreview: ../qml/ProtonUI/fontawesome.ttf ../qml/ImportExportUI/images ../qml/ProtonUI/images
rm -f ../qml/*.qmlc ../qml/ProtonUI/*.qmlc ../qml/ImportExportUI/*.qmlc
qmlscene -verbose -I ../qml/ -f ../qml/tst_GuiIE.qml 2>&1
test: qmlcheck moc.go rcc.cpp
go test -v
moc.go: ${MocDependencies}
qtmoc
clean:
rm -rf linux/
rm -rf darwin/
rm -rf windows/
rm -rf deploy/
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,55 @@
# ProtonMail Import-Export Qt interface
Import-Export uses [Qt](https://www.qt.io) framework for creating appealing graphical
user interface. Package [therecipe/qt](https://github.com/therecipe/qt) is used
to implement Qt into [Go](https://www.goglang.com).
# For developers
The GUI is designed inside QML files. Communication with backend is done via
[frontend.go](./frontend.go). The API documentation is done via `go-doc`.
## Setup
* if you don't have the system wide `go-1.8.1` download, install localy (e.g.
`~/build/go-1.8.1`) and setup:
export GOROOT=~/build/go-1.8.1/go
export PATH=$GOROOT/bin:$PATH
* go to your working directory and export `$GOPATH`
export GOPATH=`Pwd`
mkdir -p $GOPATH/bin
export PATH=$PATH:$GOPATH/bin
* if you dont have system wide `Qt-5.8.0`
[download](https://download.qt.io/official_releases/qt/5.8/5.8.0/qt-opensource-linux-x64-5.8.0.run),
install locally (e.g. `~/build/qt/qt-5.8.0`) and setup:
export QT_DIR=~/build/qt/qt-5.8.0
export PATH=$QT_DIR/5.8/gcc_64/bin:$PATH
* `Go-Qt` setup (installation is system dependent see
[therecipe/qt/README](https://github.com/therecipe/qt/blob/master/README.md)
for details)
go get -u -v github.com/therecipe/qt/cmd/...
$GOPATH/bin/qtsetup
## Compile
* it is necessary to compile the Qt-C++ with go for resources and meta-objects
make -f Makefile.local
* FIXME the rcc file is implicitly generated with `package main`. This needs to
be changed to `package qtie` manually
* check that user interface is working
make -f Makefile.local test
## Test
make -f Makefile.local qmlpreview
## Deploy
* before compilation of Import-Export it is necessary to run compilation of Qt-C++ part (done in makefile)

View File

@ -0,0 +1,68 @@
// 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 qtie
import (
"github.com/therecipe/qt/core"
)
// Folder Type
const (
FolderTypeSystem = ""
FolderTypeLabel = "label"
FolderTypeFolder = "folder"
FolderTypeExternal = "external"
)
// Status
const (
StatusNoInternet = "noInternet"
StatusCheckingInternet = "internetCheck"
StatusNewVersionAvailable = "oldVersion"
StatusUpToDate = "upToDate"
StatusForceUpdate = "forceupdate"
)
// Constants for data map
const (
// Account info
Account = int(core.Qt__UserRole) + 1<<iota
Status
Password
Aliases
IsExpanded
// Folder info
FolderId
FolderName
FolderColor
FolderType
FolderEntries
IsFolderSelected
FolderFromDate
FolderToDate
TargetFolderID
TargetLabelIDs
// Error list
MailSubject
MailDate
MailFrom
InputFolder
ErrorMessage
)

View File

@ -0,0 +1,129 @@
// 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 qtie
import (
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/therecipe/qt/core"
)
// ErrorDetail stores information about email and error
type ErrorDetail struct {
MailSubject, MailDate, MailFrom, InputFolder, ErrorMessage string
}
func init() {
ErrorListModel_QRegisterMetaType()
}
// ErrorListModel to sending error details to Qt
type ErrorListModel struct {
core.QAbstractListModel
// Qt list model
_ func() `constructor:"init"`
_ map[int]*core.QByteArray `property:"roles"`
_ int `property:"count"`
Details []*ErrorDetail
}
func (s *ErrorListModel) init() {
s.SetRoles(map[int]*core.QByteArray{
MailSubject: qtcommon.NewQByteArrayFromString("mailSubject"),
MailDate: qtcommon.NewQByteArrayFromString("mailDate"),
MailFrom: qtcommon.NewQByteArrayFromString("mailFrom"),
InputFolder: qtcommon.NewQByteArrayFromString("inputFolder"),
ErrorMessage: qtcommon.NewQByteArrayFromString("errorMessage"),
})
// basic QAbstractListModel mehods
s.ConnectData(s.data)
s.ConnectRowCount(s.rowCount)
s.ConnectColumnCount(s.columnCount)
s.ConnectRoleNames(s.roleNames)
}
func (s *ErrorListModel) data(index *core.QModelIndex, role int) *core.QVariant {
if !index.IsValid() {
return core.NewQVariant()
}
if index.Row() >= len(s.Details) {
return core.NewQVariant()
}
var p = s.Details[index.Row()]
switch role {
case MailSubject:
return qtcommon.NewQVariantString(p.MailSubject)
case MailDate:
return qtcommon.NewQVariantString(p.MailDate)
case MailFrom:
return qtcommon.NewQVariantString(p.MailFrom)
case InputFolder:
return qtcommon.NewQVariantString(p.InputFolder)
case ErrorMessage:
return qtcommon.NewQVariantString(p.ErrorMessage)
default:
return core.NewQVariant()
}
}
func (s *ErrorListModel) rowCount(parent *core.QModelIndex) int { return len(s.Details) }
func (s *ErrorListModel) columnCount(parent *core.QModelIndex) int { return 1 }
func (s *ErrorListModel) roleNames() map[int]*core.QByteArray { return s.Roles() }
// Add more errors to list
func (s *ErrorListModel) Add(more []*ErrorDetail) {
s.BeginInsertRows(core.NewQModelIndex(), len(s.Details), len(s.Details))
s.Details = append(s.Details, more...)
s.SetCount(len(s.Details))
s.EndInsertRows()
}
// Clear removes all items in model
func (s *ErrorListModel) Clear() {
s.BeginRemoveRows(core.NewQModelIndex(), 0, len(s.Details))
s.Details = s.Details[0:0]
s.SetCount(len(s.Details))
s.EndRemoveRows()
}
func (s *ErrorListModel) load(importLogFileName string) {
/*
err := backend.LoopDetailsInFile(importLogFileName, func(d *backend.MessageDetails) {
if d.MessageID != "" { // imported ok
return
}
ed := &ErrorDetail{
MailSubject: d.Subject,
MailDate: d.Time,
MailFrom: d.From,
InputFolder: d.Folder,
ErrorMessage: d.Error,
}
s.Add([]*ErrorDetail{ed})
})
if err != nil {
log.Errorf("load import report from %q: %v", importLogFileName, err)
}
*/
}

View File

@ -0,0 +1,125 @@
// 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 qtie
import (
"github.com/ProtonMail/proton-bridge/internal/transfer"
)
const (
TypeEML = "EML"
TypeMBOX = "MBOX"
)
func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
var err error
defer func() {
if err != nil {
f.showError(err)
f.Qml.ExportStructureLoadFinished(false)
} else {
f.Qml.ExportStructureLoadFinished(true)
}
}()
if f.transfer, err = f.ie.GetEMLExporter(addressOrID, ""); err != nil {
return
}
f.PMStructure.Clear()
sourceMailboxes, err := f.transfer.SourceMailboxes()
if err != nil {
return
}
for _, mbox := range sourceMailboxes {
rule := f.transfer.GetRule(mbox)
f.PMStructure.addEntry(newFolderInfo(mbox, rule))
}
f.PMStructure.transfer = f.transfer
}
func (f *FrontendQt) StartExport(rootPath, login, fileType string, attachEncryptedBody bool) {
var target transfer.TargetProvider
if fileType == TypeEML {
target = transfer.NewEMLProvider(rootPath)
} else if fileType == TypeMBOX {
target = transfer.NewMBOXProvider(rootPath)
} else {
log.Errorln("Wrong file format:", fileType)
return
}
f.transfer.ChangeTarget(target)
f.transfer.SetSkipEncryptedMessages(!attachEncryptedBody)
progress := f.transfer.Start()
f.setProgressManager(progress)
/*
TODO
f.Qml.SetProgress(0.0)
f.Qml.SetProgressDescription(backend.ProgressInit)
f.Qml.SetTotal(0)
settings := backend.ExportSettings{
FilePath: fpath,
Login: login,
AttachEncryptedBody: attachEncryptedBody,
DateBegin: 0,
DateEnd: 0,
Labels: make(map[string]string),
}
if fileType == "EML" {
settings.FileTypeID = backend.EMLFormat
} else if fileType == "MBOX" {
settings.FileTypeID = backend.MBOXFormat
} else {
log.Errorln("Wrong file format:", fileType)
return
}
username, _, err := backend.ExtractUsername(login)
if err != nil {
log.Error("qtfrontend: cannot retrieve username from alias: ", err)
return
}
settings.User, err = backend.ExtractCurrentUser(username)
if err != nil && !errors.IsCode(err, errors.ErrUnlockUser) {
return
}
for _, entity := range f.PMStructure.entities {
if entity.IsFolderSelected {
settings.Labels[entity.FolderName] = entity.FolderId
}
}
settings.DateBegin = f.PMStructure.GlobalOptions.FromDate
settings.DateEnd = f.PMStructure.GlobalOptions.ToDate
settings.PM = backend.NewProcessManager()
f.setHandlers(settings.PM)
log.Debugln("start export", settings.FilePath)
go backend.Export(f.panicHandler, settings)
*/
}

View File

@ -0,0 +1,539 @@
// 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 qtie
import (
"errors"
"strings"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/therecipe/qt/core"
)
const (
GlobalOptionIndex = -1
)
var AllFolderInfoRoles = []int{
FolderId,
FolderName,
FolderColor,
FolderType,
FolderEntries,
IsFolderSelected,
FolderFromDate,
FolderToDate,
TargetFolderID,
TargetLabelIDs,
}
func getTargetHashes(mboxes []transfer.Mailbox) (targetFolderID, targetLabelIDs string) {
for _, targetMailbox := range mboxes {
if targetMailbox.IsExclusive {
targetFolderID = targetMailbox.Hash()
} else {
targetLabelIDs += targetMailbox.Hash() + ";"
}
}
targetLabelIDs = strings.Trim(targetLabelIDs, ";")
return
}
func isSystemMailbox(mbox transfer.Mailbox) bool {
return pmapi.IsSystemLabel(mbox.ID)
}
func newFolderInfo(mbox transfer.Mailbox, rule *transfer.Rule) *FolderInfo {
targetFolderID, targetLabelIDs := getTargetHashes(rule.TargetMailboxes)
entry := &FolderInfo{
mailbox: mbox,
FolderEntries: 1,
FromDate: rule.FromTime,
ToDate: rule.ToTime,
IsFolderSelected: rule.Active,
TargetFolderID: targetFolderID,
TargetLabelIDs: targetLabelIDs,
}
entry.FolderType = FolderTypeSystem
if !isSystemMailbox(mbox) {
if mbox.IsExclusive {
entry.FolderType = FolderTypeFolder
} else {
entry.FolderType = FolderTypeLabel
}
}
return entry
}
func (s *FolderStructure) saveRule(info *FolderInfo) error {
if s.transfer == nil {
return errors.New("missing transfer")
}
sourceMbox := info.mailbox
if !info.IsFolderSelected {
s.transfer.UnsetRule(sourceMbox)
return nil
}
allTargetMboxes, err := s.transfer.TargetMailboxes()
if err != nil {
return err
}
var targetMboxes []transfer.Mailbox
for _, target := range allTargetMboxes {
targetHash := target.Hash()
if info.TargetFolderID == targetHash || strings.Contains(info.TargetLabelIDs, targetHash) {
targetMboxes = append(targetMboxes, target)
}
}
return s.transfer.SetRule(sourceMbox, targetMboxes, info.FromDate, info.ToDate)
}
func (s *FolderInfo) updateTgtLblIDs(targetLabelsSet map[string]struct{}) {
targets := []string{}
for key := range targetLabelsSet {
targets = append(targets, key)
}
s.TargetLabelIDs = strings.Join(targets, ";")
}
func (s *FolderInfo) clearTgtLblIDs() {
s.TargetLabelIDs = ""
}
func (s *FolderInfo) AddTargetLabel(targetID string) {
if targetID == "" {
return
}
targetLabelsSet := s.getSetOfLabels()
targetLabelsSet[targetID] = struct{}{}
s.updateTgtLblIDs(targetLabelsSet)
}
func (s *FolderInfo) RemoveTargetLabel(targetID string) {
if targetID == "" {
return
}
targetLabelsSet := s.getSetOfLabels()
delete(targetLabelsSet, targetID)
s.updateTgtLblIDs(targetLabelsSet)
}
func (s *FolderInfo) IsType(askType string) bool {
return s.FolderType == askType
}
func (s *FolderInfo) getSetOfLabels() (uniqSet map[string]struct{}) {
uniqSet = make(map[string]struct{})
for _, label := range s.TargetLabelIDList() {
uniqSet[label] = struct{}{}
}
return
}
func (s *FolderInfo) TargetLabelIDList() []string {
return strings.FieldsFunc(
s.TargetLabelIDs,
func(c rune) bool { return c == ';' },
)
}
// Get data
func (s *FolderStructure) data(index *core.QModelIndex, role int) *core.QVariant {
row, isValid := index.Row(), index.IsValid()
if !isValid || row >= s.getCount() {
log.Warnln("Wrong index", isValid, row)
return core.NewQVariant()
}
var f = s.get(row)
switch role {
case FolderId:
return qtcommon.NewQVariantString(f.mailbox.Hash())
case FolderName, int(core.Qt__DisplayRole):
return qtcommon.NewQVariantString(f.mailbox.Name)
case FolderColor:
return qtcommon.NewQVariantString(f.mailbox.Color)
case FolderType:
return qtcommon.NewQVariantString(f.FolderType)
case FolderEntries:
return qtcommon.NewQVariantInt(f.FolderEntries)
case FolderFromDate:
return qtcommon.NewQVariantLong(f.FromDate)
case FolderToDate:
return qtcommon.NewQVariantLong(f.ToDate)
case IsFolderSelected:
return qtcommon.NewQVariantBool(f.IsFolderSelected)
case TargetFolderID:
return qtcommon.NewQVariantString(f.TargetFolderID)
case TargetLabelIDs:
return qtcommon.NewQVariantString(f.TargetLabelIDs)
default:
log.Warnln("Wrong role", role)
return core.NewQVariant()
}
}
// Get header data (table view, tree view)
func (s *FolderStructure) headerData(section int, orientation core.Qt__Orientation, role int) *core.QVariant {
if role != int(core.Qt__DisplayRole) {
return core.NewQVariant()
}
if orientation == core.Qt__Horizontal {
return qtcommon.NewQVariantString("Column")
}
return qtcommon.NewQVariantString("Row")
}
// Flags is editable
func (s *FolderStructure) flags(index *core.QModelIndex) core.Qt__ItemFlag {
if !index.IsValid() {
return core.Qt__ItemIsEnabled
}
// can do here also: core.NewQAbstractItemModelFromPointer(s.Pointer()).Flags(index) | core.Qt__ItemIsEditable
// or s.FlagsDefault(index) | core.Qt__ItemIsEditable
return core.Qt__ItemIsEnabled | core.Qt__ItemIsSelectable | core.Qt__ItemIsEditable
}
// Set data
func (s *FolderStructure) setData(index *core.QModelIndex, value *core.QVariant, role int) bool {
log.Debugf("SET DATA %d", role)
if !index.IsValid() {
return false
}
if index.Row() < GlobalOptionIndex || index.Row() > s.getCount() || index.Column() != 1 {
return false
}
item := s.get(index.Row())
t := true
switch role {
case FolderId, FolderType:
log.
WithField("structure", s).
WithField("row", index.Row()).
WithField("column", index.Column()).
WithField("role", role).
WithField("isEdit", role == int(core.Qt__EditRole)).
Warn("Set constant role forbiden")
case FolderName:
item.mailbox.Name = value.ToString()
case FolderColor:
item.mailbox.Color = value.ToString()
case FolderEntries:
item.FolderEntries = value.ToInt(&t)
case FolderFromDate:
item.FromDate = value.ToLongLong(&t)
case FolderToDate:
item.ToDate = value.ToLongLong(&t)
case IsFolderSelected:
item.IsFolderSelected = value.ToBool()
case TargetFolderID:
item.TargetFolderID = value.ToString()
case TargetLabelIDs:
item.TargetLabelIDs = value.ToString()
default:
log.Debugln("uknown role ", s, index.Row(), index.Column(), role, role == int(core.Qt__EditRole))
return false
}
s.changedEntityRole(index.Row(), index.Row(), role)
return true
}
// Dimension of model: number of rows is equivalent to number of items in list
func (s *FolderStructure) rowCount(parent *core.QModelIndex) int {
return s.getCount()
}
func (s *FolderStructure) getCount() int {
return len(s.entities)
}
// Returns names of available item properties
func (s *FolderStructure) roleNames() map[int]*core.QByteArray {
return s.Roles()
}
// Clear removes all items in model
func (s *FolderStructure) Clear() {
s.BeginResetModel()
if s.getCount() != 0 {
s.entities = []*FolderInfo{}
}
s.GlobalOptions = FolderInfo{
mailbox: transfer.Mailbox{
Name: "=",
},
FromDate: 0,
ToDate: 0,
TargetFolderID: "",
TargetLabelIDs: "",
}
s.EndResetModel()
}
// Method connected to addEntry slot
func (s *FolderStructure) addEntry(entry *FolderInfo) {
s.insertEntry(entry, s.getCount())
}
// NewUniqId which is not in map yet.
func (s *FolderStructure) newUniqId() (name string) {
name = s.GlobalOptions.mailbox.Name
mbox := transfer.Mailbox{Name: name}
for newVal := byte(name[0]); true; newVal++ {
mbox.Name = string([]byte{newVal})
if s.getRowById(mbox.Hash()) < GlobalOptionIndex {
return
}
}
return
}
// Method connected to addEntry slot
func (s *FolderStructure) insertEntry(entry *FolderInfo, i int) {
s.BeginInsertRows(core.NewQModelIndex(), i, i)
s.entities = append(s.entities[:i], append([]*FolderInfo{entry}, s.entities[i:]...)...)
s.EndInsertRows()
// update global if conflict
if entry.mailbox.Hash() == s.GlobalOptions.mailbox.Hash() {
globalName := s.newUniqId()
s.GlobalOptions.mailbox.Name = globalName
}
}
func (s *FolderStructure) GetInfo(row int) FolderInfo {
return *s.get(row)
}
func (s *FolderStructure) changedEntityRole(rowStart int, rowEnd int, roles ...int) {
if rowStart < GlobalOptionIndex || rowEnd < GlobalOptionIndex {
return
}
if rowStart < 0 || rowStart >= s.getCount() {
rowStart = 0
}
if rowEnd < 0 || rowEnd >= s.getCount() {
rowEnd = s.getCount()
}
if rowStart > rowEnd {
tmp := rowStart
rowStart = rowEnd
rowEnd = tmp
}
indexStart := s.Index(rowStart, 0, core.NewQModelIndex())
indexEnd := s.Index(rowEnd, 0, core.NewQModelIndex())
s.updateSelection(indexStart, indexEnd, roles)
s.DataChanged(indexStart, indexEnd, roles)
}
func (s *FolderStructure) setFolderSelection(id string, toSelect bool) {
log.Debugf("set folder selection %q %b", id, toSelect)
i := s.getRowById(id)
//
info := s.get(i)
before := info.IsFolderSelected
info.IsFolderSelected = toSelect
if err := s.saveRule(info); err != nil {
s.get(i).IsFolderSelected = before
log.WithError(err).WithField("id", id).WithField("toSelect", toSelect).Error("Cannot set selection")
return
}
//
s.changedEntityRole(i, i, IsFolderSelected)
}
func (s *FolderStructure) setTargetFolderID(id, target string) {
log.Debugf("set targetFolderID %q %q", id, target)
i := s.getRowById(id)
//
info := s.get(i)
//s.get(i).TargetFolderID = target
before := info.TargetFolderID
info.TargetFolderID = target
if err := s.saveRule(info); err != nil {
info.TargetFolderID = before
log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
return
}
//
s.changedEntityRole(i, i, TargetFolderID)
if target == "" { // do not import
before := info.TargetLabelIDs
info.clearTgtLblIDs()
if err := s.saveRule(info); err != nil {
info.TargetLabelIDs = before
log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
return
}
s.changedEntityRole(i, i, TargetLabelIDs)
}
}
func (s *FolderStructure) addTargetLabelID(id, label string) {
log.Debugf("add target label id %q %q", id, label)
if label == "" {
return
}
i := s.getRowById(id)
info := s.get(i)
before := info.TargetLabelIDs
info.AddTargetLabel(label)
if err := s.saveRule(info); err != nil {
info.TargetLabelIDs = before
log.WithError(err).WithField("id", id).WithField("label", label).Error("Cannot add label")
return
}
s.changedEntityRole(i, i, TargetLabelIDs)
}
func (s *FolderStructure) removeTargetLabelID(id, label string) {
log.Debugf("remove label id %q %q", id, label)
if label == "" {
return
}
i := s.getRowById(id)
info := s.get(i)
before := info.TargetLabelIDs
info.RemoveTargetLabel(label)
if err := s.saveRule(info); err != nil {
info.TargetLabelIDs = before
log.WithError(err).WithField("id", id).WithField("label", label).Error("Cannot remove label")
return
}
s.changedEntityRole(i, i, TargetLabelIDs)
}
func (s *FolderStructure) setFromToDate(id string, from, to int64) {
log.Debugf("set from to date %q %d %d", id, from, to)
i := s.getRowById(id)
info := s.get(i)
beforeFrom := info.FromDate
beforeTo := info.ToDate
info.FromDate = from
info.ToDate = to
if err := s.saveRule(info); err != nil {
info.FromDate = beforeFrom
info.ToDate = beforeTo
log.WithError(err).WithField("id", id).WithField("from", from).WithField("to", to).Error("Cannot set date")
return
}
s.changedEntityRole(i, i, FolderFromDate, FolderToDate)
}
func (s *FolderStructure) selectType(folderType string, toSelect bool) {
log.Debugf("set type %q %b", folderType, toSelect)
iFirst, iLast := -1, -1
for i, entity := range s.entities {
if entity.IsType(folderType) {
if iFirst == -1 {
iFirst = i
}
before := entity.IsFolderSelected
entity.IsFolderSelected = toSelect
if err := s.saveRule(entity); err != nil {
entity.IsFolderSelected = before
log.WithError(err).WithField("i", i).WithField("type", folderType).WithField("toSelect", toSelect).Error("Cannot select type")
}
iLast = i
}
}
if iFirst != -1 {
s.changedEntityRole(iFirst, iLast, IsFolderSelected)
}
}
func (s *FolderStructure) updateSelection(topLeft *core.QModelIndex, bottomRight *core.QModelIndex, roles []int) {
for _, role := range roles {
switch role {
case IsFolderSelected:
s.SetSelectedFolders(true)
s.SetSelectedLabels(true)
s.SetAtLeastOneSelected(false)
for _, entity := range s.entities {
if entity.IsFolderSelected {
s.SetAtLeastOneSelected(true)
} else {
if entity.IsType(FolderTypeFolder) {
s.SetSelectedFolders(false)
}
if entity.IsType(FolderTypeLabel) {
s.SetSelectedLabels(false)
}
}
if !s.IsSelectedFolders() && !s.IsSelectedLabels() && s.IsAtLeastOneSelected() {
break
}
}
default:
}
}
}
func (s *FolderStructure) hasFolderWithName(name string) bool {
for _, entity := range s.entities {
if entity.mailbox.Name == name {
return true
}
}
return false
}
func (s *FolderStructure) getRowById(id string) (row int) {
for row = GlobalOptionIndex; row < s.getCount(); row++ {
if id == s.get(row).mailbox.Hash() {
return
}
}
row = GlobalOptionIndex - 1
return
}
func (s *FolderStructure) hasTarget() bool {
for row := 0; row < s.getCount(); row++ {
if s.get(row).TargetFolderID != "" {
return true
}
}
return false
}
// Getter for account info pointer
// index out of array length returns empty folder info to avoid segfault
// index == GlobalOptionIndex is set to access global options
func (s *FolderStructure) get(index int) *FolderInfo {
if index < GlobalOptionIndex || index >= s.getCount() {
return &FolderInfo{}
}
if index == GlobalOptionIndex {
return &s.GlobalOptions
}
return s.entities[index]
}

View File

@ -0,0 +1,196 @@
// 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 qtie
// TODO:
// Proposal for new structure
// It will be a bit more memory but much better performance
// * Rules:
// * rules []Rule /QAbstracItemModel/
// * globalFromDate int64
// * globalToDate int64
// * globalLabel Mbox
// * targetPath string
// * filterEncryptedBodies bool
// * Rule
// * sourceMbox: Mbox
// * targetFolders: []Mbox /QAbstracItemModel/ (all available target folders)
// * targetLabels: []Mbox /QAbstracItemModel/ (all available target labels)
// * selectedLabelColors: QStringList (need reset context on change) (show label list)
// * fromDate int64
// * toDate int64
// * Mbox
// * IsActive bool (show checkox)
// * Name string (show name)
// * Type string (show icon)
// * Color string (show icon)
//
// Biggest update: add folder or label for all roles update target models
import (
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/therecipe/qt/core"
)
// FolderStructure model providing container for items (folder info) to QML
//
// QML ListView connects the model from Go and it shows item (entities)
// information.
//
// Copied and edited from `github.com/therecipe/qt/internal/examples/sailfish/listview`
//
// NOTE: When implementing a model it is important to remember that QAbstractItemModel does not store any data itself !!!!
// see https://doc.qt.io/qt-5/model-view-programming.html#designing-a-model
type FolderStructure 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 entities. It is not accessed directly but using
// `data(index,role)`
entities []*FolderInfo
GlobalOptions FolderInfo
transfer *transfer.Transfer
// Global Folders/Labels selection flag, use setter from QML
_ bool `property:"selectedLabels"`
_ bool `property:"selectedFolders"`
_ bool `property:"atLeastOneSelected"`
// Getters (const)
_ func() int `slot:"getCount"`
_ func(index int) string `slot:"getID"`
_ func(id string) string `slot:"getName"`
_ func(id string) string `slot:"getType"`
_ func(id string) string `slot:"getColor"`
_ func(id string) int64 `slot:"getFrom"`
_ func(id string) int64 `slot:"getTo"`
_ func(id string) string `slot:"getTargetLabelIDs"`
_ func(name string) bool `slot:"hasFolderWithName"`
_ func() bool `slot:"hasTarget"`
// TODO get folders
// TODO get labels
// TODO get selected labels
// TODO get selected folder
// Setters (emits DataChanged)
_ func(fileType string, toSelect bool) `slot:"selectType"`
_ func(id string, toSelect bool) `slot:"setFolderSelection"`
_ func(id string, target string) `slot:"setTargetFolderID"`
_ func(id string, label string) `slot:"addTargetLabelID"`
_ func(id string, label string) `slot:"removeTargetLabelID"`
_ func(id string, from, to int64) `slot:"setFromToDate"`
}
// FolderInfo is the element of model
//
// It contains all data for one structure entry
type FolderInfo struct {
/*
FolderId string
FolderFullPath string
FolderColor string
FolderFullName string
*/
mailbox transfer.Mailbox // TODO how to reference from qml source mailbox to go target mailbox
FolderType string
FolderEntries int // todo remove
IsFolderSelected bool
FromDate int64 // Unix seconds
ToDate int64 // Unix seconds
TargetFolderID string // target ID TODO: this will be hash
TargetLabelIDs string // semicolon separated list of label ID same here
}
// Registration of new metatype before creating instance
//
// NOTE: check it is run once per program. write a log
func init() {
FolderStructure_QRegisterMetaType()
}
// Constructor
//
// Creates the map for item properties and connects the methods
func (s *FolderStructure) init() {
s.SetRoles(map[int]*core.QByteArray{
FolderId: qtcommon.NewQByteArrayFromString("folderId"),
FolderName: qtcommon.NewQByteArrayFromString("folderName"),
FolderColor: qtcommon.NewQByteArrayFromString("folderColor"),
FolderType: qtcommon.NewQByteArrayFromString("folderType"),
FolderEntries: qtcommon.NewQByteArrayFromString("folderEntries"),
IsFolderSelected: qtcommon.NewQByteArrayFromString("isFolderSelected"),
FolderFromDate: qtcommon.NewQByteArrayFromString("fromDate"),
FolderToDate: qtcommon.NewQByteArrayFromString("toDate"),
TargetFolderID: qtcommon.NewQByteArrayFromString("targetFolderID"),
TargetLabelIDs: qtcommon.NewQByteArrayFromString("targetLabelIDs"),
})
// basic QAbstractListModel mehods
s.ConnectGetCount(s.getCount)
s.ConnectRowCount(s.rowCount)
s.ConnectColumnCount(func(parent *core.QModelIndex) int { return 1 }) // for list it should be always 1
s.ConnectData(s.data)
s.ConnectHeaderData(s.headerData)
s.ConnectRoleNames(s.roleNames)
// Editable QAbstractListModel needs: https://doc.qt.io/qt-5/model-view-programming.html#an-editable-model
s.ConnectSetData(s.setData)
s.ConnectFlags(s.flags)
// Custom FolderStructure slots to export
// Getters (const)
s.ConnectGetID(func(row int) string { return s.get(row).mailbox.Hash() })
s.ConnectGetType(func(id string) string { row := s.getRowById(id); return s.get(row).FolderType })
s.ConnectGetName(func(id string) string { row := s.getRowById(id); return s.get(row).mailbox.Name })
s.ConnectGetColor(func(id string) string { row := s.getRowById(id); return s.get(row).mailbox.Color })
s.ConnectGetFrom(func(id string) int64 { row := s.getRowById(id); return s.get(row).FromDate })
s.ConnectGetTo(func(id string) int64 { row := s.getRowById(id); return s.get(row).ToDate })
s.ConnectGetTargetLabelIDs(func(id string) string { row := s.getRowById(id); return s.get(row).TargetLabelIDs })
s.ConnectHasFolderWithName(s.hasFolderWithName)
s.ConnectHasTarget(s.hasTarget)
// Setters (emits DataChanged)
s.ConnectSelectType(s.selectType)
s.ConnectSetFolderSelection(s.setFolderSelection)
s.ConnectSetTargetFolderID(s.setTargetFolderID)
s.ConnectAddTargetLabelID(s.addTargetLabelID)
s.ConnectRemoveTargetLabelID(s.removeTargetLabelID)
s.ConnectSetFromToDate(s.setFromToDate)
s.GlobalOptions = FolderInfo{
mailbox: transfer.Mailbox{Name: "="},
FromDate: 0,
ToDate: 0,
TargetFolderID: "",
TargetLabelIDs: "",
}
}

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/>.
// +build !nogui
package qtie
import (
"testing"
)
func hasNumberOfLabels(tb testing.TB, folder *FolderInfo, expected int) {
if current := len(folder.TargetLabelIDList()); current != expected {
tb.Error("Folder has wrong number of labels. Expected", expected, "has", current, " labels", folder.TargetLabelIDs)
}
}
func labelStringEquals(tb testing.TB, folder *FolderInfo, expected string) {
if current := folder.TargetLabelIDs; current != expected {
tb.Error("Folder returned wrong labels. Expected", expected, "has", current, " labels", folder.TargetLabelIDs)
}
}
func TestLabelInfoUniqSet(t *testing.T) {
folder := &FolderInfo{}
labelStringEquals(t, folder, "")
hasNumberOfLabels(t, folder, 0)
// add label
folder.AddTargetLabel("blah")
hasNumberOfLabels(t, folder, 1)
labelStringEquals(t, folder, "blah")
//
folder.AddTargetLabel("blah___")
hasNumberOfLabels(t, folder, 2)
labelStringEquals(t, folder, "blah;blah___")
// add same label
folder.AddTargetLabel("blah")
hasNumberOfLabels(t, folder, 2)
// remove label
folder.RemoveTargetLabel("blah")
hasNumberOfLabels(t, folder, 1)
//
folder.AddTargetLabel("blah___")
hasNumberOfLabels(t, folder, 1)
// remove same label
folder.RemoveTargetLabel("blah")
hasNumberOfLabels(t, folder, 1)
// add again label
folder.AddTargetLabel("blah")
hasNumberOfLabels(t, folder, 2)
}

View File

@ -0,0 +1,497 @@
// 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 qtie
import (
"errors"
"os"
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/therecipe/qt/core"
"github.com/therecipe/qt/gui"
"github.com/therecipe/qt/qml"
"github.com/therecipe/qt/widgets"
"github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
)
var log = logrus.WithField("pkg", "frontend-qt-ie")
// FrontendQt is API between Import-Export 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 {
panicHandler types.PanicHandler
config *config.Config
eventListener listener.Listener
updates types.Updater
ie types.ImportExporter
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 qtcommon.Accounts // Providing data for accounts ListView
programName string // Program name
programVersion string // Program version
buildVersion string // Program build version
PMStructure *FolderStructure // Providing data for account labels and folders for ProtonMail account
ExternalStructure *FolderStructure // Providing data for account labels and folders for MBOX, EML or external IMAP account
ErrorList *ErrorListModel // Providing data for error reporting
transfer *transfer.Transfer
notifyHasNoKeychain bool
}
// New is constructor for Import-Export Qt-Go interface
func New(
version, buildVersion string,
panicHandler types.PanicHandler,
config *config.Config,
eventListener listener.Listener,
updates types.Updater,
ie types.ImportExporter,
) *FrontendQt {
f := &FrontendQt{
panicHandler: panicHandler,
config: config,
programName: "ProtonMail Import-Export",
programVersion: "v" + version,
eventListener: eventListener,
buildVersion: buildVersion,
updates: updates,
ie: ie,
}
// Nicer string for OS
currentOS := core.QSysInfo_PrettyProductName()
ie.SetCurrentOS(currentOS)
log.Debugf("New Qt frontend: %p", f)
return f
}
// IsAppRestarting for Import-Export is always false i.e never restarts
func (s *FrontendQt) IsAppRestarting() bool {
return false
}
// Loop function for Import-Export interface. It runs QtExecute in main thread
// with no additional function.
func (s *FrontendQt) Loop(setupError error) (err error) {
if setupError != 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() {
internetOffCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.InternetOffEvent)
internetOnCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.InternetOnEvent)
restartBridgeCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.RestartBridgeEvent)
addressChangedCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.AddressChangedEvent)
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.AddressChangedLogoutEvent)
logoutCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.LogoutEvent)
updateApplicationCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.UpgradeApplicationEvent)
newUserCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.UserRefreshEvent)
for {
select {
case <-internetOffCh:
s.Qml.SetConnectionStatus(false)
case <-internetOnCh:
s.Qml.SetConnectionStatus(true)
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.ie.GetUser(userID)
if err != nil {
return
}
s.Qml.NotifyLogout(user.Username())
case <-updateApplicationCh:
s.Qml.ProcessFinished()
s.Qml.NotifyUpdate()
case <-newUserCh:
s.Qml.LoadAccounts()
}
}
}
func (s *FrontendQt) qtSetupQmlAndStructures() {
s.App = widgets.NewQApplication(len(os.Args), os.Args)
// view
s.View = qml.NewQQmlApplicationEngine(s.App)
// Add Go-QML Import-Export
s.Qml = NewGoQMLInterface(nil)
s.Qml.SetFrontend(s) // provides access
s.View.RootContext().SetContextProperty("go", s.Qml)
// Add AccountsModel
s.Accounts.SetupAccounts(s.Qml, s.ie)
s.View.RootContext().SetContextProperty("accountsModel", s.Accounts.Model)
// Add ProtonMail FolderStructure
s.PMStructure = NewFolderStructure(nil)
s.View.RootContext().SetContextProperty("structurePM", s.PMStructure)
// Add external FolderStructure
s.ExternalStructure = NewFolderStructure(nil)
s.View.RootContext().SetContextProperty("structureExternal", s.ExternalStructure)
// Add error list modal
s.ErrorList = NewErrorListModel(nil)
s.View.RootContext().SetContextProperty("errorList", s.ErrorList)
s.Qml.ConnectLoadImportReports(s.ErrorList.load)
// Import path and load QML files
s.View.AddImportPath("qrc:///")
s.View.Load(core.NewQUrl3("qrc:/uiie.qml", 0))
// TODO set the first start flag
log.Error("Get FirstStart: Not implemented")
//if prefs.Get(prefs.FirstStart) == "true" {
if false {
s.Qml.SetIsFirstStart(true)
} else {
s.Qml.SetIsFirstStart(false)
}
// Notify user about error during initialization.
if s.notifyHasNoKeychain {
s.Qml.NotifyHasNoKeychain()
}
}
// QtExecute in main for starting Qt application
//
// It is needed 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 {
qtcommon.QtSetupCoreAndControls(s.programName, s.programVersion)
s.qtSetupQmlAndStructures()
// Check QML is loaded properly
if len(s.View.RootObjects()) == 0 {
//return errors.New(errors.ErrQApplication, "QML not loaded properly")
return errors.New("QML not loaded properly")
}
// Obtain main window (need for invoke method)
s.MainWin = s.View.RootObjects()[0]
// 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(errors.ErrQApplication, "Event loop ended with return value: %v", string(ret))
err := errors.New("Event loop ended with return value: " + string(ret))
log.Warnln("QGuiApplication_Exec: ", err)
return err
}
log.Debug("Closing...")
log.Error("Set FirstStart: Not implemented")
//prefs.Set(prefs.FirstStart, "false")
return nil
}
func (s *FrontendQt) openLogs() {
go open.Run(s.config.GetLogDir())
}
func (s *FrontendQt) openReport() {
go open.Run(s.Qml.ImportLogFileName())
}
func (s *FrontendQt) openDownloadLink() {
go open.Run(s.updates.GetDownloadLink())
}
func (s *FrontendQt) sendImportReport(address, reportFile string) (isOK bool) {
/*
accname := "[No account logged in]"
if s.Accounts.Count() > 0 {
accname = s.Accounts.get(0).Account()
}
basename := filepath.Base(reportFile)
req := pmapi.ReportReq{
OS: core.QSysInfo_ProductType(),
OSVersion: core.QSysInfo_PrettyProductName(),
Title: "[Import Export] Import report: " + basename,
Description: "Sending import report file in attachment.",
Username: accname,
Email: address,
}
report, err := os.Open(reportFile)
if err != nil {
log.Errorln("report file open:", err)
isOK = false
}
req.AddAttachment("log", basename, report)
c := pmapi.NewClient(backend.APIConfig, "import_reporter")
err = c.Report(req)
if err != nil {
log.Errorln("while sendReport:", err)
isOK = false
return
}
log.Infof("Report %q send successfully", basename)
isOK = true
*/
return false
}
// sendBug is almost idetical to bridge
func (s *FrontendQt) sendBug(description, emailClient, address string) (isOK bool) {
isOK = true
var accname = "No account logged in"
if s.Accounts.Model.Count() > 0 {
accname = s.Accounts.Model.Get(0).Account()
}
if err := s.ie.ReportBug(
core.QSysInfo_ProductType(),
core.QSysInfo_PrettyProductName(),
description,
accname,
address,
emailClient,
); err != nil {
log.Errorln("while sendBug:", err)
isOK = false
}
return
}
// checkInternet is almost idetical to bridge
func (s *FrontendQt) checkInternet() {
s.Qml.SetConnectionStatus(s.ie.CheckConnection() == nil)
}
func (s *FrontendQt) showError(err error) {
code := 0 // TODO err.Code()
s.Qml.SetErrorDescription(err.Error())
log.WithField("code", code).Errorln(err.Error())
s.Qml.NotifyError(code)
}
func (s *FrontendQt) emitEvent(evType, msg string) {
s.eventListener.Emit(evType, msg)
}
func (s *FrontendQt) setProgressManager(progress *transfer.Progress) {
s.Qml.ConnectPauseProcess(func() { progress.Pause("user") })
s.Qml.ConnectResumeProcess(progress.Resume)
s.Qml.ConnectCancelProcess(func(clearUnfinished bool) {
// TODO clear unfinished
progress.Stop()
})
go func() {
defer func() {
s.Qml.DisconnectPauseProcess()
s.Qml.DisconnectResumeProcess()
s.Qml.DisconnectCancelProcess()
s.Qml.SetProgress(1)
}()
//TODO get log file (in old code it was here, but this is ugly place probably somewhere else)
updates := progress.GetUpdateChannel()
for range updates {
if progress.IsStopped() {
break
}
failed, imported, _, _, total := progress.GetCounts()
if total != 0 { // udate total
s.Qml.SetTotal(int(total))
}
s.Qml.SetProgressFails(int(failed))
s.Qml.SetProgressDescription(progress.PauseReason()) // TODO add description when changing folders?
if total > 0 {
newProgress := float32(imported+failed) / float32(total)
if newProgress >= 0 && newProgress != s.Qml.Progress() {
s.Qml.SetProgress(newProgress)
s.Qml.ProgressChanged(newProgress)
}
}
}
// TODO fatal error?
}()
}
// StartUpdate is identical to bridge
func (s *FrontendQt) StartUpdate() {
progress := make(chan updates.Progress)
go func() { // Update progress in QML.
defer s.panicHandler.HandlePanic()
for current := range progress {
s.Qml.SetProgress(current.Processed)
s.Qml.SetProgressDescription(strconv.Itoa(current.Description))
// Error happend
if current.Err != nil {
log.Error("update progress: ", current.Err)
s.Qml.UpdateFinished(true)
return
}
// Finished everything OK.
if current.Description >= updates.InfoQuitApp {
s.Qml.UpdateFinished(false)
time.Sleep(3 * time.Second) // Just notify.
s.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp)
s.App.Quit()
return
}
}
}()
go func() {
defer s.panicHandler.HandlePanic()
s.updates.StartUpgrade(progress)
}()
}
// isNewVersionAvailable is identical to bridge
// return 0 when local version is fine
// return 1 when new version is available
func (s *FrontendQt) isNewVersionAvailable(showMessage bool) {
go func() {
defer s.Qml.ProcessFinished()
isUpToDate, latestVersionInfo, err := s.updates.CheckIsUpToDate()
if err != nil {
log.Warnln("Cannot retrieve version info: ", err)
s.checkInternet()
return
}
s.Qml.SetConnectionStatus(true) // if we are here connection is ok
if isUpToDate {
s.Qml.SetUpdateState(StatusUpToDate)
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.SetUpdateState(StatusNewVersionAvailable)
}()
}
func (s *FrontendQt) resetSource() {
if s.transfer != nil {
s.transfer.ResetRules()
if err := s.loadStructuresForImport(); err != nil {
log.WithError(err).Error("Cannot reload structures after reseting rules.")
}
}
}
// getLocalVersionInfo is identical to bridge.
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)
}
// LeastUsedColor is intended to return color for creating a new inbox or label.
func (s *FrontendQt) leastUsedColor() string {
if s.transfer == nil {
log.Errorln("Getting least used color before transfer exist.")
return "#7272a7"
}
m, err := s.transfer.TargetMailboxes()
if err != nil {
log.Errorln("Getting least used color:", err)
s.showError(err)
}
return transfer.LeastUsedColor(m)
}
// createLabelOrFolder performs an IE target mailbox creation.
func (s *FrontendQt) createLabelOrFolder(email, name, color string, isLabel bool, sourceID string) bool {
// Prepare new mailbox.
m := transfer.Mailbox{
Name: name,
Color: color,
IsExclusive: !isLabel,
}
// Select least used color if no color given.
if m.Color == "" {
m.Color = s.leastUsedColor()
}
// Create mailbox.
newLabel, err := s.transfer.CreateTargetMailbox(m)
if err != nil {
log.Errorln("Folder/Label creating:", err)
s.showError(err)
return false
}
// TODO: notify UI of newly added folders/labels
/*errc := s.PMStructure.Load(email, false)
if errc != nil {
s.showError(errc)
return false
}*/
if sourceID != "" {
if isLabel {
s.ExternalStructure.addTargetLabelID(sourceID, newLabel.ID)
} else {
s.ExternalStructure.setTargetFolderID(sourceID, newLabel.ID)
}
}
return 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/>.
// +build nogui
package qtie
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"
"github.com/sirupsen/logrus"
)
var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
type FrontendHeadless struct{}
func (s *FrontendHeadless) Loop(credentialsError error) error {
log.Info("Check status on localhost:8081")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "IE is running")
})
return http.ListenAndServe(":8081", nil)
}
func (s *FrontendHeadless) IsAppRestarting() bool { return false }
func New(
version, buildVersion string,
panicHandler types.PanicHandler,
config *config.Config,
eventListener listener.Listener,
updates types.Updater,
ie types.ImportExporter,
) *FrontendHeadless {
return &FrontendHeadless{}
}

View File

@ -0,0 +1,89 @@
// 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 qtie
import "github.com/ProtonMail/proton-bridge/internal/transfer"
// wrapper for QML
func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetAddress string) {
var err error
defer func() {
if err != nil {
f.showError(err)
f.Qml.ImportStructuresLoadFinished(false)
} else {
f.Qml.ImportStructuresLoadFinished(true)
}
}()
if isFromIMAP {
f.transfer, err = f.ie.GetRemoteImporter(targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort)
if err != nil {
return
}
} else {
f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath)
if err != nil {
return
}
}
if err := f.loadStructuresForImport(); err != nil {
return
}
}
func (f *FrontendQt) loadStructuresForImport() error {
f.PMStructure.Clear()
targetMboxes, err := f.transfer.TargetMailboxes()
if err != nil {
return err
}
for _, mbox := range targetMboxes {
rule := &transfer.Rule{}
f.PMStructure.addEntry(newFolderInfo(mbox, rule))
}
f.ExternalStructure.Clear()
sourceMboxes, err := f.transfer.SourceMailboxes()
if err != nil {
return err
}
for _, mbox := range sourceMboxes {
rule := f.transfer.GetRule(mbox)
f.ExternalStructure.addEntry(newFolderInfo(mbox, rule))
}
f.ExternalStructure.transfer = f.transfer
return nil
}
func (f *FrontendQt) StartImport(email string) { // TODO email not needed
f.Qml.SetProgressDescription("init") // TODO use const
f.Qml.SetProgressFails(0)
f.Qml.SetProgress(0.0)
f.Qml.SetTotal(1)
f.Qml.SetImportLogFileName("")
f.ErrorList.Clear()
progress := f.transfer.Start()
f.setProgressManager(progress)
}

View File

@ -0,0 +1,32 @@
// 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 qtie
const (
TabGlobal = 0
TabSettings = 1
TabHelp = 2
TabQuit = 4
TabAddAccount = -1
)
func (s *FrontendQt) SendNotification(tabIndex int, msg string) {
s.Qml.NotifyBubble(tabIndex, msg)
}

View File

@ -0,0 +1,25 @@
// 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 qtie
type panicHandler interface {
HandlePanic()
SendReport(interface{})
}

View File

@ -0,0 +1,189 @@
// 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 qtie
import (
"runtime"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/therecipe/qt/core"
)
// GoQMLInterface between go and qml
//
// Here we implements all the signals / methods.
type GoQMLInterface struct {
core.QObject
_ func() `constructor:"init"`
_ string `property:"currentAddress"`
_ string `property:"goos"`
_ bool `property:"isFirstStart"`
_ bool `property:"isRestarting"`
_ bool `property:"isConnectionOK"`
_ string `property:lastError`
_ float32 `property:progress`
_ string `property:progressDescription`
_ int `property:progressFails`
_ int `property:total`
_ string `property:importLogFileName`
_ string `property:"programTitle"`
_ string `property:"newversion"`
_ 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"`
//
_ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func(updateState string) `signal:"setUpdateState"`
_ func() `slot:"checkInternet"`
_ func() `signal:"processFinished"`
_ func(okay bool) `signal:"exportStructureLoadFinished"`
_ func(okay bool) `signal:"importStructuresLoadFinished"`
_ func() `signal:"openManual"`
_ func(showMessage bool) `signal:"runCheckVersion"`
_ func() `slot:"getLocalVersionInfo"`
_ func(fname string) `slot:"loadImportReports"`
_ func() `slot:"quit"`
_ func() `slot:"loadAccounts"`
_ func() `slot:"openLogs"`
_ func() `slot:"openDownloadLink"`
_ func() `slot:"openReport"`
_ func() `slot:"clearCache"`
_ func() `slot:"clearKeychain"`
_ func() `signal:"highlightSystray"`
_ func() `signal:"normalSystray"`
_ func(showMessage bool) `slot:"isNewVersionAvailable"`
_ func() string `slot:"getBackendVersion"`
_ func(description, client, address string) bool `slot:"sendBug"`
_ func(address, fname string) bool `slot:"sendImportReport"`
_ func(address string) `slot:"loadStructureForExport"`
_ func() string `slot:"leastUsedColor"`
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
_ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"`
_ func(email string) `slot:"startImport"`
_ func() `slot:"resetSource"`
_ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetAddress string) `slot:"setupAndLoadForImport"`
_ string `property:"progressInit"`
_ func(path string) int `slot:"checkPathStatus"`
_ func(evType string, msg string) `signal:"emitEvent"`
_ func(tabIndex int, message string) `signal:"notifyBubble"`
_ func() `signal:"bubbleClosed"`
_ func() `signal:"simpleErrorHappen"`
_ func() `signal:"askErrorHappen"`
_ func() `signal:"retryErrorHappen"`
_ func() `signal:"pauseProcess"`
_ func() `signal:"resumeProcess"`
_ func(clearUnfinished bool) `signal:"cancelProcess"`
_ func(iAccount int, prefRem bool) `slot:"deleteAccount"`
_ func(iAccount int) `slot:"logoutAccount"`
_ 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() `slot:"startUpdate"`
_ func(hasError bool) `signal:"updateFinished"`
// errors
_ func() `signal:"answerRetry"`
_ func(all bool) `signal:"answerSkip"`
_ func(errCode int) `signal:"notifyError"`
_ string `property:"errorDescription"`
}
// Constructor
func (s *GoQMLInterface) init() {}
// SetFrontend connects all slots and signals from Go to QML
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectQuit(f.App.Quit)
s.ConnectLoadAccounts(f.Accounts.LoadAccounts)
s.ConnectOpenLogs(f.openLogs)
s.ConnectOpenDownloadLink(f.openDownloadLink)
s.ConnectOpenReport(f.openReport)
s.ConnectClearCache(f.Accounts.ClearCache)
s.ConnectClearKeychain(f.Accounts.ClearKeychain)
s.ConnectSendBug(f.sendBug)
s.ConnectSendImportReport(f.sendImportReport)
s.ConnectDeleteAccount(f.Accounts.DeleteAccount)
s.ConnectLogoutAccount(f.Accounts.LogoutAccount)
s.ConnectLogin(f.Accounts.Login)
s.ConnectAuth2FA(f.Accounts.Auth2FA)
s.ConnectAddAccount(f.Accounts.AddAccount)
s.SetGoos(runtime.GOOS)
s.SetIsRestarting(false)
s.SetProgramTitle(f.programName)
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
s.ConnectGetBackendVersion(func() string {
return f.programVersion
})
s.ConnectCheckInternet(f.checkInternet)
s.ConnectLoadStructureForExport(f.LoadStructureForExport)
s.ConnectSetupAndLoadForImport(f.setupAndLoadForImport)
s.ConnectResetSource(f.resetSource)
s.ConnectLeastUsedColor(f.leastUsedColor)
s.ConnectCreateLabelOrFolder(f.createLabelOrFolder)
s.ConnectStartExport(f.StartExport)
s.ConnectStartImport(f.StartImport)
s.ConnectCheckPathStatus(qtcommon.CheckPathStatus)
s.ConnectStartUpdate(f.StartUpdate)
s.ConnectEmitEvent(f.emitEvent)
}