Import/Export final touches

This commit is contained in:
Michal Horejsek
2020-08-12 13:56:49 +02:00
parent 4f0af0fb02
commit 658ead9fb3
82 changed files with 451 additions and 450 deletions

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Wed 29 Jul 2020 10:20:09 AM CEST. DO NOT EDIT.
// Code generated by ./credits.sh at Wed Aug 12 09:33:24 CEST 2020. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-delve/delve;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/psampaz/go-mod-outdated;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -24,12 +24,23 @@ import (
"runtime/pprof"
)
// StartCPUProfile starts CPU pprof.
func StartCPUProfile() {
f, err := os.Create("./cpu.pprof")
if err != nil {
log.Fatal("Could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("Could not start CPU profile: ", err)
}
}
// MakeMemoryProfile generates memory pprof.
func MakeMemoryProfile() {
name := "./mem.pprof"
f, err := os.Create(name)
if err != nil {
log.Error("Could not create memory profile: ", err)
log.Fatal("Could not create memory profile: ", err)
}
if abs, err := filepath.Abs(name); err == nil {
name = abs
@ -37,7 +48,7 @@ func MakeMemoryProfile() {
log.Info("Writing memory profile to ", name)
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Error("Could not write memory profile: ", err)
log.Fatal("Could not write memory profile: ", err)
}
_ = f.Close()
}

View File

@ -17,7 +17,7 @@
package cmd
import "github.com/ProtonMail/proton-bridge/pkg/updates"
import "github.com/ProtonMail/proton-bridge/internal/updates"
// GenerateVersionFiles writes a JSON file with details about current build.
// Those files are used for upgrading the app.

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
package cliie
import (
"fmt"

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
package cliie
import (
"strings"

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package cli provides CLI interface of the Import/Export.
package cli
// Package cliie provides CLI interface of the Import-Export app.
package cliie
import (
"github.com/ProtonMail/proton-bridge/internal/events"
@ -68,7 +68,7 @@ func New( //nolint[funlen]
Aliases: []string{"cl"},
}
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: k, keychain)",
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts,
})
@ -77,7 +77,7 @@ func New( //nolint[funlen]
// Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
Help: "check for Import/Export updates. (aliases: u, v, version)",
Help: "check for Import-Export updates. (aliases: u, v, version)",
Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates,
})
@ -134,7 +134,7 @@ func New( //nolint[funlen]
Completer: fe.completeUsernames,
})
// Import/Export commands.
// Import-Export commands.
importCmd := &ishell.Cmd{Name: "import",
Help: "import messages. (alias: imp)",
Aliases: []string{"imp"},
@ -167,7 +167,7 @@ func New( //nolint[funlen]
// System commands.
fe.AddCmd(&ishell.Cmd{Name: "restart",
Help: "restart the import/export.",
Help: "restart the Import-Export app.",
Func: fe.restart,
})
@ -190,7 +190,7 @@ func (f *frontendCLI) watchEvents() {
for {
select {
case errorDetails := <-errorCh:
f.Println("Import/Export failed:", errorDetails)
f.Println("Import-Export failed:", errorDetails)
case <-internetOffCh:
f.notifyInternetOff()
case <-internetOnCh:
@ -228,9 +228,9 @@ func (f *frontendCLI) Loop(credentialsError error) error {
}
f.Print(`
Welcome to ProtonMail Import/Export interactive shell
Welcome to ProtonMail Import-Export interactive shell
WARNING: CLI is experimental feature and does not cover all functionality yet.
WARNING: The CLI is an experimental feature and does not yet cover all functionality.
`)
f.Run()
return nil

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
package cliie
import (
"fmt"

View File

@ -15,19 +15,15 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
package cliie
import (
"github.com/abiosoft/ishell"
)
var (
currentPort = "" //nolint[gochecknoglobals]
)
func (f *frontendCLI) restart(c *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to restart the Import/Export") {
f.Println("Restarting Import/Export...")
if f.yesNoQuestion("Are you sure you want to restart the Import-Export") {
f.Println("Restarting the Import-Export app...")
f.appRestart = true
f.Stop()
}
@ -37,7 +33,7 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.ie.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check you internet connection.")
f.Println("Can not contact the server, please check your internet connection.")
}
}
@ -46,5 +42,5 @@ func (f *frontendCLI) printLogDir(c *ishell.Context) {
}
func (f *frontendCLI) printManual(c *ishell.Context) {
f.Println("More instructions about the Import/Export can be found at\n\n https://protonmail.com/support/categories/import-export/")
f.Println("More instructions about the Import-Export app can be found at\n\n https://protonmail.com/support/categories/import-export/")
}

View File

@ -15,13 +15,13 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
package cliie
import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/abiosoft/ishell"
)
@ -47,7 +47,7 @@ func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) {
}
func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
f.Println(bold("ProtonMail Import/Export "+versionInfo.Version), "\n")
f.Println(bold("ProtonMail Import-Export "+versionInfo.Version), "\n")
if versionInfo.ReleaseNotes != "" {
f.Println(bold("Release Notes"))
f.Println(versionInfo.ReleaseNotes)

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
package cliie
import (
"strings"
@ -98,7 +98,7 @@ func (f *frontendCLI) notifyNeedUpgrade() {
func (f *frontendCLI) notifyCredentialsError() {
// Print in 80-column width.
f.Println("ProtonMail Import/Export is not able to detect a supported password manager")
f.Println("ProtonMail Import-Export is not able to detect a supported password manager")
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")
f.Println("and restart the application.")
}
@ -109,7 +109,7 @@ func (f *frontendCLI) notifyCertIssue() {
be insecure.
Description:
ProtonMail Import/Export was not able to establish a secure connection to Proton
ProtonMail Import-Export was not able to establish a secure connection to Proton
servers due to a TLS certificate error. This means your connection may
potentially be insecure and susceptible to monitoring by third parties.

View File

@ -76,7 +76,7 @@ func New( //nolint[funlen]
Func: fe.deleteCache,
})
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: k, keychain)",
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts,
})

View File

@ -43,7 +43,7 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.bridge.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check you internet connection.")
f.Println("Can not contact the server, please check your internet connection.")
}
}

View File

@ -21,7 +21,7 @@ import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/abiosoft/ishell"
)

View File

@ -458,7 +458,7 @@ Dialog {
if (progressbarImport.isFinished) return qsTr("Import finished","todo")
if (
go.progressDescription == gui.enums.progressInit ||
(go.progress == 0 && go.description=="")
(go.progress == 0 && go.progressDescription=="")
) return qsTr("Estimating the total number of messages","todo")
if (
go.progressDescription == gui.enums.progressLooping

View File

@ -43,6 +43,8 @@ Rectangle {
property string lastTargetFolder: "6" // Archive
property string lastTargetLabels: "" // no flag by default
property string sourceID : mboxID
property string sourceName : name
Rectangle {
id: line
@ -71,7 +73,7 @@ Rectangle {
Text {
id: folderIcon
text : gui.folderIcon(name, gui.enums.folderTypeFolder)
text : gui.folderIcon(root.sourceName, gui.enums.folderTypeFolder)
anchors.verticalCenter : parent.verticalCenter
color: root.isSourceSelected ? Style.main.text : Style.main.textDisabled
font {
@ -81,7 +83,7 @@ Rectangle {
}
Text {
text : name
text : root.sourceName
width: nameWidth
elide: Text.ElideRight
anchors.verticalCenter : parent.verticalCenter
@ -102,8 +104,8 @@ Rectangle {
SelectFolderMenu {
id: selectFolder
sourceID: mboxID
targets: transferRules.targetFolders(mboxID)
sourceID: root.sourceID
targets: transferRules.targetFolders(root.sourceID)
width: nameWidth
anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
@ -112,8 +114,8 @@ Rectangle {
}
SelectLabelsMenu {
sourceID: mboxID
targets: transferRules.targetLabels(mboxID)
sourceID: root.sourceID
targets: transferRules.targetLabels(root.sourceID)
width: nameWidth
anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
@ -130,7 +132,7 @@ Rectangle {
DateRangeMenu {
id: dateRangeMenu
sourceID: mboxID
sourceID: root.sourceID
sourceFromDate: fromDate
sourceToDate: toDate
@ -143,10 +145,10 @@ Rectangle {
function importToFolder(newTargetID) {
transferRules.addTargetID(mboxID,newTargetID)
transferRules.addTargetID(root.sourceID,newTargetID)
}
function toggleImport() {
transferRules.setIsRuleActive(mboxID, !root.isSourceSelected)
transferRules.setIsRuleActive(root.sourceID, !root.isSourceSelected)
}
}

View File

@ -110,7 +110,7 @@ Rectangle {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: {
if (listview.currentIndex<0) return 0
if (listview.currentItem === null) return 0
else return listview.currentItem.leftMargin1
}
}

View File

@ -112,7 +112,7 @@ Window {
rightMargin: innerWindowBorder
}
model: [
{ "title" : qsTr("Import/Export" , "title of tab that shows account list" ), "iconText": Style.fa.home },
{ "title" : qsTr("Import-Export" , "title of tab that shows account list" ), "iconText": Style.fa.home },
{ "title" : qsTr("Settings" , "title of tab that allows user to change settings" ), "iconText": Style.fa.cogs },
{ "title" : qsTr("Help" , "title of tab that shows the help menu" ), "iconText": Style.fa.life_ring }
]
@ -381,8 +381,9 @@ Window {
onClickedNo: popupMessage.hide()
onClickedOkay: popupMessage.hide()
onClickedCancel: popupMessage.hide()
onClickedYes: {
if (popupMessage.message == gui.areYouSureYouWantToQuit) Qt.quit()
if (popupMessage.text == gui.areYouSureYouWantToQuit) Qt.quit()
}
}
@ -461,8 +462,9 @@ Window {
(dialogExport.visible && dialogExport.currentIndex == 2 && go.progress!=1)
) {
popupMessage.buttonOkay .visible = false
popupMessage.buttonNo .visible = true
popupMessage.buttonYes .visible = true
popupMessage.buttonYes .visible = false
popupMessage.buttonQuit .visible = true
popupMessage.buttonCancel .visible = true
popupMessage.show ( gui.areYouSureYouWantToQuit )
return
}

View File

@ -58,7 +58,6 @@ ComboBox {
}
displayText: {
console.log("Target Menu current", view.currentItem, view.currentIndex)
if (view.currentIndex >= 0) {
if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels")

View File

@ -25,6 +25,7 @@ Rectangle {
color: Style.transparent
property alias text : message.text
property alias checkbox : checkbox
property alias buttonQuit : buttonQuit
property alias buttonOkay : buttonOkay
property alias buttonYes : buttonYes
property alias buttonNo : buttonNo
@ -89,13 +90,13 @@ Rectangle {
spacing: Style.dialog.spacing
anchors.horizontalCenter : parent.horizontalCenter
ButtonRounded { id : buttonQuit ; text : qsTr ( "Stop & quit", "" ) ; onClicked : root.clickedYes ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonNo ; text : qsTr ( "No" , "Button No" ) ; onClicked : root.clickedNo ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonYes ; text : qsTr ( "Yes" , "Button Yes" ) ; onClicked : root.clickedYes ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonRetry ; text : qsTr ( "Retry" , "Button Retry" ) ; onClicked : root.clickedRetry ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonSkip ; text : qsTr ( "Skip" , "Button Skip" ) ; onClicked : root.clickedSkip ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonCancel ; text : qsTr ( "Cancel" , "Button Cancel" ) ; onClicked : root.clickedCancel ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonOkay ; text : qsTr ( "Okay" , "Button Okay" ) ; onClicked : root.clickedOkay ( ) ; visible : true ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
}
}
}

View File

@ -99,7 +99,7 @@ Window {
id: buttons
ListElement { title : "Show window" }
ListElement { title : "Logout cuthix" }
ListElement { title : "Logout" }
ListElement { title : "Internet on" }
ListElement { title : "Internet off" }
ListElement { title : "Macos" }
@ -143,8 +143,8 @@ Window {
case "Show window" :
go.showWindow();
break;
case "Logout cuthix" :
go.checkLoggedOut("cuthix");
case "Logout" :
go.checkLoggedOut("ie");
break;
case "Internet on" :
go.setConnectionStatus(true);
@ -223,10 +223,10 @@ Window {
ListModel{
id: accountsModel
ListElement{ account : "cuthix" ; status : "connected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "cuto@pm.com;jaku@pm.com;DoYouKnowAboutAMovieCalledTheHorriblySlowMurderWithExtremelyInefficientWeapon@thatYouCanFindForExampleOnyoutube.com" }
ListElement{ account : "exteremelongnamewhichmustbeeladedinthemiddleoftheaddress@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "cuto@pm.com;jaku@pm.com;hu@hu.hu" }
ListElement{ account : "cuthix2@protonmail.com" ; status : "disconnected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "cuto@pm.com;jaku@pm.com;hu@hu.hu" }
ListElement{ account : "many@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;"}
ListElement{ account : "ie" ; status : "connected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "ie@pm.com;jaku@pm.com;DoYouKnowAboutAMovieCalledTheHorriblySlowMurderWithExtremelyInefficientWeapon@thatYouCanFindForExampleOnyoutube.com" }
ListElement{ account : "exteremelongnamewhichmustbeeladedinthemiddleoftheaddress@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "ie@pm.com;jaku@pm.com;hu@hu.hu" }
ListElement{ account : "ie2@protonmail.com" ; status : "disconnected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "ie@pm.com;jaku@pm.com;hu@hu.hu" }
ListElement{ account : "many@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;"}
}
ListModel{
@ -830,9 +830,9 @@ Window {
property string bugNotSent
property string bugReportSent
property string programTitle : "ProtonMail Import/Export Tool"
property string programTitle : "ProtonMail Import-Export App"
property string newversion : "q0.1.0"
property string landingPage : "https://jakub.cuth.sk/bridge"
property string landingPage : "https://landing.page"
property string changelog : "• Lorem ipsum dolor sit amet\n• consetetur sadipscing elitr,\n• sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,\n• sed diam voluptua.\n• At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
//property string changelog : ""
property string bugfixes : "• lorem ipsum dolor sit amet;• consetetur sadipscing elitr;• sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat;• sed diam voluptua;• at vero eos et accusam et justo duo dolores et ea rebum;• stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet"

View File

@ -103,16 +103,6 @@ func PauseLong() {
time.Sleep(3 * time.Second)
}
func ParsePMAPIError(err error, code int) error {
/*
if err == pmapi.ErrAPINotReachable {
code = ErrNoInternet
}
return errors.NewFromError(code, err)
*/
return nil
}
// FIXME: Not working in test...
func WaitForEnter() {
log.Print("Press 'Enter' to continue...")

View File

@ -47,6 +47,18 @@ func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
return
}
// Export has only one option to set time limits--by global time range.
// In case user changes file or because of some bug global time is saved
// to all rules, let's clear it, because there is no way to show it in
// GUI and user would be confused and see it does not work at all.
for _, rule := range f.transfer.GetRules() {
isActive := rule.Active
f.transfer.SetRule(rule.SourceMailbox, rule.TargetMailboxes, 0, 0)
if !isActive {
f.transfer.UnsetRule(rule.SourceMailbox)
}
}
f.TransferRules.setTransfer(f.transfer)
}
@ -65,55 +77,4 @@ func (f *FrontendQt) StartExport(rootPath, login, fileType string, attachEncrypt
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

@ -59,10 +59,6 @@ func getTargetHashes(mboxes []transfer.Mailbox) (targetFolderID, 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)
@ -77,7 +73,7 @@ func newFolderInfo(mbox transfer.Mailbox, rule *transfer.Rule) *FolderInfo {
}
entry.FolderType = FolderTypeSystem
if !isSystemMailbox(mbox) {
if !pmapi.IsSystemLabel(mbox.ID) {
if mbox.IsExclusive {
entry.FolderType = FolderTypeFolder
} else {
@ -112,7 +108,7 @@ func (s *FolderStructure) saveRule(info *FolderInfo) error {
return s.transfer.SetRule(sourceMbox, targetMboxes, info.FromDate, info.ToDate)
}
func (s *FolderInfo) updateTgtLblIDs(targetLabelsSet map[string]struct{}) {
func (s *FolderInfo) updateTargetLabelIDs(targetLabelsSet map[string]struct{}) {
targets := []string{}
for key := range targetLabelsSet {
targets = append(targets, key)
@ -120,17 +116,13 @@ func (s *FolderInfo) updateTgtLblIDs(targetLabelsSet map[string]struct{}) {
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)
s.updateTargetLabelIDs(targetLabelsSet)
}
func (s *FolderInfo) RemoveTargetLabel(targetID string) {
@ -139,7 +131,7 @@ func (s *FolderInfo) RemoveTargetLabel(targetID string) {
}
targetLabelsSet := s.getSetOfLabels()
delete(targetLabelsSet, targetID)
s.updateTgtLblIDs(targetLabelsSet)
s.updateTargetLabelIDs(targetLabelsSet)
}
func (s *FolderInfo) IsType(askType string) bool {
@ -387,7 +379,7 @@ func (s *FolderStructure) setTargetFolderID(id, target string) {
s.changedEntityRole(i, i, TargetFolderID)
if target == "" { // do not import
before := info.TargetLabelIDs
info.clearTgtLblIDs()
info.TargetLabelIDs = ""
if err := s.saveRule(info); err != nil {
info.TargetLabelIDs = before
log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")

View File

@ -29,9 +29,9 @@ import (
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/internal/updates"
"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"
@ -185,7 +185,7 @@ func (f *FrontendQt) qtSetupQmlAndStructures() {
f.View.Load(core.NewQUrl3("qrc:/uiie.qml", 0))
// TODO set the first start flag
log.Error("Get FirstStart: Not implemented")
//log.Error("Get FirstStart: Not implemented")
//if prefs.Get(prefs.FirstStart) == "true" {
if false {
f.Qml.SetIsFirstStart(true)
@ -226,7 +226,6 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
return err
}
log.Debug("Closing...")
log.Error("Set FirstStart: Not implemented")
//prefs.Set(prefs.FirstStart, "false")
return nil
}
@ -318,27 +317,31 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
f.Qml.ConnectCancelProcess(func() {
progress.Stop()
})
f.Qml.SetProgress(0)
go func() {
log.Trace("Start reading updates")
defer func() {
log.Trace("Finishing reading updates")
f.Qml.DisconnectPauseProcess()
f.Qml.DisconnectResumeProcess()
f.Qml.DisconnectCancelProcess()
f.Qml.SetProgress(1)
f.progress = nil
f.ErrorList.Progress = nil
}()
//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
if total != 0 {
f.Qml.SetTotal(int(total))
}
f.Qml.SetProgressFails(int(failed))
f.Qml.SetProgressDescription(progress.PauseReason()) // TODO add description when changing folders?
f.Qml.SetProgressDescription(progress.PauseReason())
if total > 0 {
newProgress := float32(imported+failed) / float32(total)
if newProgress >= 0 && newProgress != f.Qml.Progress() {
@ -436,7 +439,7 @@ func (f *FrontendQt) getLocalVersionInfo() {
// LeastUsedColor is intended to return color for creating a new inbox or label.
func (f *FrontendQt) leastUsedColor() string {
if f.transfer == nil {
log.Errorln("Getting least used color before transfer exist.")
log.Warnln("Getting least used color before transfer exist.")
return "#7272a7"
}

View File

@ -74,6 +74,8 @@ func (f *FrontendQt) loadStructuresForImport() error {
}
func (f *FrontendQt) StartImport(email string) { // TODO email not needed
log.Trace("Starting import")
f.Qml.SetProgressDescription("init") // TODO use const
f.Qml.SetProgressFails(0)
f.Qml.SetProgress(0.0)

View File

@ -55,8 +55,8 @@ func newMboxList(t *TransferRules, rule *transfer.Rule, containsFolders bool) *M
m.log = log.
WithField("rule", m.rule.SourceMailbox.Hash()).
WithField("folders", m.containsFolders)
m.updateSelectedIndex()
m.EndResetModel()
m.itemsChanged(rule)
return m
}
@ -71,11 +71,6 @@ func (m *MboxList) rowCount(index *core.QModelIndex) int {
}
func (m *MboxList) roleNames() map[int]*core.QByteArray {
m.log.
WithField("isActive", MboxIsActive).
WithField("id", MboxID).
WithField("color", MboxColor).
Debug("role names")
return map[int]*core.QByteArray{
MboxIsActive: qtcommon.NewQByteArrayFromString("isActive"),
MboxID: qtcommon.NewQByteArrayFromString("mboxID"),
@ -88,17 +83,17 @@ func (m *MboxList) roleNames() map[int]*core.QByteArray {
func (m *MboxList) data(index *core.QModelIndex, role int) *core.QVariant {
allTargets := m.targetMailboxes()
i, valid := index.Row(), index.IsValid()
l := m.log.WithField("row", i).WithField("role", role)
l.Trace("called data()")
i := index.Row()
log := m.log.WithField("row", i).WithField("role", role)
log.Trace("Mbox data")
if !valid || i >= len(allTargets) {
l.WithField("row", i).Warning("Invalid index")
if i >= len(allTargets) {
log.Warning("Invalid index")
return core.NewQVariant()
}
if m.transfer == nil {
l.Warning("Requested mbox list data before transfer is connected")
log.Warning("Requested mbox list data before transfer is connected")
return qtcommon.NewQVariantString("")
}
@ -131,7 +126,7 @@ func (m *MboxList) data(index *core.QModelIndex, role int) *core.QVariant {
return qtcommon.NewQVariantString(mbox.Color)
default:
l.Error("Requested mbox list data with unknown role")
log.Error("Requested mbox list data with unknown role")
return qtcommon.NewQVariantString("")
}
}
@ -161,11 +156,10 @@ func (m *MboxList) filter(mailboxes []transfer.Mailbox) (filtered []transfer.Mai
func (m *MboxList) itemsChanged(rule *transfer.Rule) {
m.rule = rule
allTargets := m.targetMailboxes()
l := m.log.WithField("count", len(allTargets))
l.Trace("called itemChanged()")
defer func() {
l.WithField("selected", m.SelectedIndex()).Trace("index updated")
}()
m.log.WithField("count", len(allTargets)).Trace("Mbox items changed")
m.updateSelectedIndex()
// NOTE: Be careful with indices: If they are invalid the DataChanged
// signal will not be sent to QML e.g. `end == rowCount - 1`
@ -175,7 +169,10 @@ func (m *MboxList) itemsChanged(rule *transfer.Rule) {
changedRoles := []int{MboxIsActive}
m.DataChanged(begin, end, changedRoles)
}
}
func (m *MboxList) updateSelectedIndex() {
allTargets := m.targetMailboxes()
for index, targetMailbox := range allTargets {
for _, selectedTarget := range m.rule.TargetMailboxes {
if targetMailbox.Hash() == selectedTarget.Hash() {

View File

@ -15,7 +15,9 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package qtcommon
// +build !nogui
package qtie
import (
"io/ioutil"

View File

@ -44,6 +44,7 @@ type TransferRules struct {
_ func(sourceID string, targetID string) `slot:"addTargetID,auto"`
_ func(sourceID string, targetID string) `slot:"removeTargetID,auto"`
// globalFromDate and globalToDate is just default value for GUI, always zero.
_ int `property:"globalFromDate"`
_ int `property:"globalToDate"`
_ bool `property:"isLabelGroupSelected"`
@ -90,21 +91,23 @@ func (t *TransferRules) roleNames() map[int]*core.QByteArray {
}
func (t *TransferRules) data(index *core.QModelIndex, role int) *core.QVariant {
i, valid := index.Row(), index.IsValid()
if !valid || i >= t.rowCount(index) {
log.WithField("row", i).Warning("Invalid index")
return core.NewQVariant()
}
i := index.Row()
allRules := t.transfer.GetRules()
log := log.WithField("row", i).WithField("role", role)
log.Trace("Transfer rules data")
if i >= len(allRules) {
log.Warning("Invalid index")
return core.NewQVariant()
}
if t.transfer == nil {
log.Warning("Requested transfer rules data before transfer is connected")
return qtcommon.NewQVariantString("")
}
rule := t.transfer.GetRules()[i]
rule := allRules[i]
switch role {
case MboxIsActive:
@ -160,6 +163,9 @@ func (t *TransferRules) setTransfer(transfer *transfer.Transfer) {
t.transfer = transfer
t.targetFoldersCache = make(map[string]*MboxList)
t.targetLabelsCache = make(map[string]*MboxList)
t.updateGroupSelection()
}
@ -196,7 +202,9 @@ func (t *TransferRules) targetLabels(sourceID string) *MboxList {
// Setters
func (t *TransferRules) setIsGroupActive(groupName string, isActive bool) {
wantExclusive := (groupName == FolderTypeLabel)
log.WithField("group", groupName).WithField("active", isActive).Trace("Setting group as active/inactive")
wantExclusive := (groupName == FolderTypeFolder)
for _, rule := range t.transfer.GetRules() {
if rule.SourceMailbox.IsExclusive != wantExclusive {
continue
@ -265,6 +273,7 @@ func (t *TransferRules) addTargetID(sourceID string, targetID string) {
newTargetMailboxes = append(newTargetMailboxes, *targetMailboxToAdd)
}
t.setRule(rule.SourceMailbox, newTargetMailboxes, rule.FromTime, rule.ToTime, []int{RuleTargetLabelColors})
t.updateTargetSelection(sourceID, targetMailboxToAdd.IsExclusive)
}
func (t *TransferRules) removeTargetID(sourceID string, targetID string) {
@ -286,10 +295,14 @@ func (t *TransferRules) removeTargetID(sourceID string, targetID string) {
}
}
t.setRule(rule.SourceMailbox, newTargetMailboxes, rule.FromTime, rule.ToTime, []int{RuleTargetLabelColors})
t.updateTargetSelection(sourceID, targetMailboxToRemove.IsExclusive)
}
// Helpers
// getRule returns rule for given source ID.
// WARN: Always get new rule after change because previous pointer points to
// outdated struct with old data.
func (t *TransferRules) getRule(sourceID string) *transfer.Rule {
mailbox := t.getMailbox(t.transfer.SourceMailboxes, sourceID)
if mailbox == nil {
@ -331,20 +344,19 @@ func (t *TransferRules) unsetRule(sourceMailbox transfer.Mailbox) {
}
func (t *TransferRules) ruleChanged(sourceMailbox transfer.Mailbox, changedRoles []int) {
for row, rule := range t.transfer.GetRules() {
allRules := t.transfer.GetRules()
for row, rule := range allRules {
if rule.SourceMailbox.Hash() != sourceMailbox.Hash() {
continue
}
t.targetFolders(sourceMailbox.Hash()).itemsChanged(rule)
t.targetLabels(sourceMailbox.Hash()).itemsChanged(rule)
index := t.Index(row, 0, core.NewQModelIndex())
if !index.IsValid() || row >= t.rowCount(index) {
if !index.IsValid() || row >= len(allRules) {
log.WithField("row", row).Warning("Invalid index")
return
}
log.WithField("row", row).Trace("Transfer rule changed")
t.DataChanged(index, index, changedRoles)
break
}
@ -375,3 +387,16 @@ func (t *TransferRules) updateGroupSelection() {
t.SetIsLabelGroupSelected(areAllLabelsSelected)
t.SetIsFolderGroupSelected(areAllFoldersSelected)
}
func (t *TransferRules) updateTargetSelection(sourceID string, updateFolderSelect bool) {
rule := t.getRule(sourceID)
if rule == nil {
return
}
if updateFolderSelect {
t.targetFolders(rule.SourceMailbox.Hash()).itemsChanged(rule)
} else {
t.targetLabels(rule.SourceMailbox.Hash()).itemsChanged(rule)
}
}

View File

@ -22,7 +22,6 @@ package qtie
import (
"runtime"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/therecipe/qt/core"
)
@ -181,7 +180,7 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectStartExport(f.StartExport)
s.ConnectStartImport(f.StartImport)
s.ConnectCheckPathStatus(qtcommon.CheckPathStatus)
s.ConnectCheckPathStatus(CheckPathStatus)
s.ConnectStartUpdate(f.StartUpdate)

View File

@ -43,15 +43,13 @@ import (
"github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/sirupsen/logrus"
//"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/sirupsen/logrus"
"github.com/kardianos/osext"
"github.com/skratchdot/open-golang/open"
"github.com/therecipe/qt/core"

View File

@ -22,8 +22,8 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/updates"
)
// PanicHandler is an interface of a type that can be used to gracefully handle panics which occur.
@ -104,7 +104,7 @@ func (b *bridgeWrap) GetUser(query string) (User, error) {
return b.Bridge.GetUser(query)
}
// ImportExporter is an interface of import/export needed by frontend.
// ImportExporter is an interface of import-export needed by frontend.
type ImportExporter interface {
UserManager
@ -121,9 +121,9 @@ type importExportWrap struct {
*importexport.ImportExport
}
// NewImportExportWrap wraps import/export struct into local importExportWrap
// NewImportExportWrap wraps import-export struct into local importExportWrap
// to implement local interface.
// The problem is that Import/Export returns the importexport package's User
// The problem is that Import-Export returns the importexport package's User
// type. Every method which returns User therefore has to be overridden to
// fulfill the interface.
func NewImportExportWrap(ie *importexport.ImportExport) *importExportWrap { //nolint[golint]

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package importexport provides core functionality of Import/Export app.
// Package importexport provides core functionality of Import-Export app.
package importexport
import (
@ -90,7 +90,7 @@ func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address strin
defer c.Logout()
title := "[Import-Export] report file"
description := "An import/export report from the user swam down the river."
description := "An Import-Export report from the user swam down the river."
report := pmapi.ReportReq{
OS: osType,
@ -120,7 +120,7 @@ func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transf
if err != nil {
return nil, err
}
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetTransferDir(), source, target)
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
}
// GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account.
@ -133,7 +133,7 @@ func (ie *ImportExport) GetRemoteImporter(address, username, password, host, por
if err != nil {
return nil, err
}
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetTransferDir(), source, target)
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
}
// GetEMLExporter returns transferrer from ProtonMail account to local EML structure.
@ -143,7 +143,7 @@ func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer
return nil, err
}
target := transfer.NewEMLProvider(path)
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetTransferDir(), source, target)
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
}
// GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure.
@ -153,7 +153,7 @@ func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfe
return nil, err
}
target := transfer.NewMBOXProvider(path)
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetTransferDir(), source, target)
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
}
func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvider, error) {

View File

@ -21,7 +21,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/store"
)
// storeFactory implements dummy factory creating no store (not needed by Import/Export).
// storeFactory implements dummy factory creating no store (not needed by Import-Export).
type storeFactory struct{}
// New does nothing.

View File

@ -22,5 +22,6 @@ import "github.com/ProtonMail/proton-bridge/internal/users"
type Configer interface {
users.Configer
GetLogDir() string
GetTransferDir() string
}

View File

@ -67,7 +67,7 @@ const (
Daily = Action("daily")
)
// Metrics related to import/export (transfer) process.
// Metrics related to import-export (transfer) process.
const (
// Import is used to group import metrics.
Import = Category("import")

View File

@ -25,6 +25,25 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
var systemFolderMapping = map[string]string{ //nolint[gochecknoglobals]
"bin": "Trash",
"junk": "Spam",
"all": "All Mail",
"sent mail": "Sent",
"draft": "Drafts",
"important": "Starred",
// Add more translations.
}
// LeastUsedColor is intended to return color for creating a new inbox or label
func LeastUsedColor(mailboxes []Mailbox) string {
usedColors := []string{}
for _, m := range mailboxes {
usedColors = append(usedColors, m.Color)
}
return pmapi.LeastUsedColor(usedColors)
}
// Mailbox is universal data holder of mailbox details for every provider.
type Mailbox struct {
ID string
@ -43,28 +62,10 @@ func (m Mailbox) Hash() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(m.Name)))
}
// LeastUsedColor is intended to return color for creating a new inbox or label
func LeastUsedColor(mailboxes []Mailbox) string {
usedColors := []string{}
for _, m := range mailboxes {
usedColors = append(usedColors, m.Color)
}
return pmapi.LeastUsedColor(usedColors)
}
// findMatchingMailboxes returns all matching mailboxes from `mailboxes`.
// Only one exclusive mailbox is returned.
// Only one exclusive mailbox is included.
func (m Mailbox) findMatchingMailboxes(mailboxes []Mailbox) []Mailbox {
nameVariants := []string{}
if strings.Contains(m.Name, "/") || strings.Contains(m.Name, "|") {
for _, slashPart := range strings.Split(m.Name, "/") {
for _, part := range strings.Split(slashPart, "|") {
nameVariants = append(nameVariants, strings.ToLower(part))
}
}
}
nameVariants = append(nameVariants, strings.ToLower(m.Name))
nameVariants := m.nameVariants()
isExclusiveIncluded := false
matches := []Mailbox{}
for i := range nameVariants {
@ -83,3 +84,27 @@ func (m Mailbox) findMatchingMailboxes(mailboxes []Mailbox) []Mailbox {
}
return matches
}
// nameVariants returns all possible variants of the mailbox name.
// The best match (original name) is at the end of the slice.
// Variants are all in lower case. Examples:
// * Foo/bar -> [foo, bar, foo/bar]
// * x/Bin -> [x, trash, bin, x/bin]
// * a|b/c -> [a, b, c, a|b/c]
func (m Mailbox) nameVariants() (nameVariants []string) {
name := strings.ToLower(m.Name)
if strings.Contains(name, "/") || strings.Contains(name, "|") {
for _, slashPart := range strings.Split(name, "/") {
for _, part := range strings.Split(slashPart, "|") {
if mappedPart, ok := systemFolderMapping[part]; ok {
nameVariants = append(nameVariants, strings.ToLower(mappedPart))
}
nameVariants = append(nameVariants, part)
}
}
}
if mappedName, ok := systemFolderMapping[name]; ok {
nameVariants = append(nameVariants, strings.ToLower(mappedName))
}
return append(nameVariants, name)
}

View File

@ -66,6 +66,7 @@ func TestLeastUsedColor(t *testing.T) {
}
r.Equal(t, "#7569d1", LeastUsedColor(mailboxes))
}
func TestFindMatchingMailboxes(t *testing.T) {
mailboxes := []Mailbox{
{Name: "Inbox", IsExclusive: true},
@ -75,6 +76,8 @@ func TestFindMatchingMailboxes(t *testing.T) {
{Name: "hello/world", IsExclusive: true},
{Name: "Hello", IsExclusive: false},
{Name: "WORLD", IsExclusive: true},
{Name: "Trash", IsExclusive: true},
{Name: "Drafts", IsExclusive: true},
}
tests := []struct {
@ -88,6 +91,10 @@ func TestFindMatchingMailboxes(t *testing.T) {
{"hello/world", []string{"hello/world", "Hello"}},
{"hello|world", []string{"WORLD", "Hello"}},
{"nomailbox", []string{}},
{"bin", []string{"Trash"}},
{"root/bin", []string{"Trash"}},
{"draft", []string{"Drafts"}},
{"root/draft", []string{"Drafts"}},
}
for _, tc := range tests {
tc := tc

View File

@ -30,11 +30,12 @@ import (
// Import and export update progress about processing messages and progress
// informs user interface, vice versa action (such as pause or resume) from
// user interface is passed down to import and export.
type Progress struct {
type Progress struct { //nolint[maligned]
log *logrus.Entry
lock sync.RWMutex
lock sync.Locker
updateCh chan struct{}
messageCounted bool
messageCounts map[string]uint
messageStatuses map[string]*MessageStatus
pauseReason string
@ -45,7 +46,8 @@ type Progress struct {
func newProgress(log *logrus.Entry, fileReport *fileReport) Progress {
return Progress{
log: log,
log: log,
lock: &sync.Mutex{},
updateCh: make(chan struct{}),
messageCounts: map[string]uint{},
@ -57,11 +59,7 @@ func newProgress(log *logrus.Entry, fileReport *fileReport) Progress {
// update is helper to notify listener for updates.
func (p *Progress) update() {
if p.updateCh == nil {
// If the progress was ended by fatal instead finish, we ignore error.
if p.fatalError != nil {
return
}
panic("update should not be called after finish was called")
return
}
// In case no one listens for an update, do not block the progress.
@ -71,17 +69,12 @@ func (p *Progress) update() {
}
}
// start should be called before anything starts.
func (p *Progress) start() {
p.lock.Lock()
defer p.lock.Unlock()
}
// finish should be called as the last call once everything is done.
func (p *Progress) finish() {
p.lock.Lock()
defer p.lock.Unlock()
log.Debug("Progress finished")
p.cleanUpdateCh()
}
@ -90,6 +83,7 @@ func (p *Progress) fatal(err error) {
p.lock.Lock()
defer p.lock.Unlock()
log.WithError(err).Error("Progress finished")
p.isStopped = true
p.fatalError = err
p.cleanUpdateCh()
@ -97,21 +91,26 @@ func (p *Progress) fatal(err error) {
func (p *Progress) cleanUpdateCh() {
if p.updateCh == nil {
// If the progress was ended by fatal instead finish, we ignore error.
if p.fatalError != nil {
return
}
panic("update should not be called after finish was called")
return
}
close(p.updateCh)
p.updateCh = nil
}
func (p *Progress) countsFinal() {
p.lock.Lock()
defer p.lock.Unlock()
defer p.update()
log.Info("Estimating count finished")
p.messageCounted = true
}
func (p *Progress) updateCount(mailbox string, count uint) {
p.lock.Lock()
defer p.update()
defer p.lock.Unlock()
defer p.update()
log.WithField("mailbox", mailbox).WithField("count", count).Debug("Mailbox count updated")
p.messageCounts[mailbox] = count
@ -120,8 +119,8 @@ func (p *Progress) updateCount(mailbox string, count uint) {
// addMessage should be called as soon as there is ID of the message.
func (p *Progress) addMessage(messageID string, rule *Rule) {
p.lock.Lock()
defer p.update()
defer p.lock.Unlock()
defer p.update()
p.log.WithField("id", messageID).Trace("Message added")
p.messageStatuses[messageID] = &MessageStatus{
@ -134,10 +133,15 @@ func (p *Progress) addMessage(messageID string, rule *Rule) {
// messageExported should be called right before message is exported.
func (p *Progress) messageExported(messageID string, body []byte, err error) {
p.lock.Lock()
defer p.update()
defer p.lock.Unlock()
defer p.update()
log := p.log.WithField("id", messageID)
if err != nil {
log = log.WithError(err)
}
log.Debug("Message exported")
p.log.WithField("id", messageID).WithError(err).Debug("Message exported")
status := p.messageStatuses[messageID]
status.exportErr = err
if err == nil {
@ -148,7 +152,7 @@ func (p *Progress) messageExported(messageID string, body []byte, err error) {
status.bodyHash = fmt.Sprintf("%x", sha256.Sum256(body))
if header, err := getMessageHeader(body); err != nil {
p.log.WithField("id", messageID).WithError(err).Warning("Failed to parse headers for reporting")
log.WithError(err).Warning("Failed to parse headers for reporting")
} else {
status.setDetailsFromHeader(header)
}
@ -163,10 +167,15 @@ func (p *Progress) messageExported(messageID string, body []byte, err error) {
// messageImported should be called right after message is imported.
func (p *Progress) messageImported(messageID, importID string, err error) {
p.lock.Lock()
defer p.update()
defer p.lock.Unlock()
defer p.update()
log := p.log.WithField("id", messageID)
if err != nil {
log = log.WithError(err)
}
log.Debug("Message imported")
p.log.WithField("id", messageID).WithError(err).Debug("Message imported")
p.messageStatuses[messageID].targetID = importID
p.messageStatuses[messageID].importErr = err
if err == nil {
@ -187,6 +196,8 @@ func (p *Progress) logMessage(messageID string) {
// callWrap calls the callback and in case of problem it pause the process.
// Then it waits for user action to fix it and click on continue or abort.
// Every function doing I/O should be wrapped by this function to provide
// stopping and pausing functionality.
func (p *Progress) callWrap(callback func() error) {
for {
if p.shouldStop() {
@ -222,8 +233,8 @@ func (p *Progress) GetUpdateChannel() chan struct{} {
// Pause pauses the progress.
func (p *Progress) Pause(reason string) {
p.lock.Lock()
defer p.update()
defer p.lock.Unlock()
defer p.update()
p.log.Info("Progress paused")
p.pauseReason = reason
@ -232,8 +243,8 @@ func (p *Progress) Pause(reason string) {
// Resume resumes the progress.
func (p *Progress) Resume() {
p.lock.Lock()
defer p.update()
defer p.lock.Unlock()
defer p.update()
p.log.Info("Progress resumed")
p.pauseReason = ""
@ -258,8 +269,8 @@ func (p *Progress) PauseReason() string {
// Stop stops the process.
func (p *Progress) Stop() {
p.lock.Lock()
defer p.update()
defer p.lock.Unlock()
defer p.update()
p.log.Info("Progress stopped")
p.isStopped = true
@ -304,6 +315,12 @@ func (p *Progress) GetCounts() (failed, imported, exported, added, total uint) {
p.lock.Lock()
defer p.lock.Unlock()
// Return counts only once total is estimated or the process already
// ended (for a case when it ended quickly to report it correctly).
if p.updateCh != nil && !p.messageCounted {
return
}
// Include lost messages in the process only when transfer is done.
includeMissing := p.updateCh == nil
@ -334,10 +351,10 @@ func (p *Progress) GenerateBugReport() []byte {
return bugReport.getData()
}
func (p *Progress) FileReport() (path string) {
if r := p.fileReport; r != nil {
path = r.path
// FileReport returns path to generated defailed file report.
func (p *Progress) FileReport() string {
if p.fileReport == nil {
return ""
}
return
return p.fileReport.path
}

View File

@ -29,8 +29,6 @@ func TestProgressUpdateCount(t *testing.T) {
progress := newProgress(log, nil)
drainProgressUpdateChannel(&progress)
progress.start()
progress.updateCount("inbox", 10)
progress.updateCount("archive", 20)
progress.updateCount("inbox", 12)
@ -48,8 +46,6 @@ func TestProgressAddingMessages(t *testing.T) {
progress := newProgress(log, nil)
drainProgressUpdateChannel(&progress)
progress.start()
// msg1 has no problem.
progress.addMessage("msg1", nil)
progress.messageExported("msg1", []byte(""), nil)
@ -92,18 +88,16 @@ func TestProgressFinish(t *testing.T) {
progress := newProgress(log, nil)
drainProgressUpdateChannel(&progress)
progress.start()
progress.finish()
r.Nil(t, progress.updateCh)
r.Panics(t, func() { progress.addMessage("msg", nil) })
r.NotPanics(t, func() { progress.addMessage("msg", nil) })
}
func TestProgressFatalError(t *testing.T) {
progress := newProgress(log, nil)
drainProgressUpdateChannel(&progress)
progress.start()
progress.fatal(errors.New("fatal error"))
r.Nil(t, progress.updateCh)

View File

@ -36,6 +36,10 @@ func (p *EMLProvider) TransferTo(rules transferRules, progress *Progress, ch cha
return
}
if len(filePathsPerFolder) == 0 {
return
}
// This list is not filtered by time but instead going throgh each file
// twice or keeping all in memory we will tell rough estimation which
// will be updated during processing each file.
@ -46,6 +50,7 @@ func (p *EMLProvider) TransferTo(rules transferRules, progress *Progress, ch cha
progress.updateCount(folderName, uint(len(filePaths)))
}
progress.countsFinal()
for folderName, filePaths := range filePathsPerFolder {
// No error guaranteed by getFilePathsPerFolder.

View File

@ -21,7 +21,6 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/go-multierror"
)
@ -73,7 +72,7 @@ func (p *EMLProvider) createFolders(rules transferRules) error {
func (p *EMLProvider) writeFile(msg Message) error {
fileName := filepath.Base(msg.ID)
if !strings.HasSuffix(fileName, ".eml") {
if filepath.Ext(fileName) != ".eml" {
fileName += ".eml"
}

View File

@ -58,7 +58,7 @@ func (p *IMAPProvider) ID() string {
// Mailboxes returns all available folder names from root of EML files.
// In case the same folder name is used more than once (for example root/a/foo
// and root/b/foo), it's treated as the same folder.
func (p *IMAPProvider) Mailboxes(includEmpty, includeAllMail bool) ([]Mailbox, error) {
func (p *IMAPProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) {
mailboxesInfo, err := p.list()
if err != nil {
return nil, err
@ -73,11 +73,11 @@ func (p *IMAPProvider) Mailboxes(includEmpty, includeAllMail bool) ([]Mailbox, e
break
}
}
if hasNoSelect || mailbox.Name == "[Gmail]" {
if hasNoSelect {
continue
}
if !includEmpty || true {
if !includeEmpty || true {
mailboxStatus, err := p.selectIn(mailbox.Name)
if err != nil {
return nil, err

View File

@ -72,6 +72,7 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres
res[rule.SourceMailbox.Name] = messagesInfo
progress.updateCount(rule.SourceMailbox.Name, uint(len(messagesInfo)))
}
progress.countsFinal()
return res
}
@ -109,7 +110,9 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
return
}
}
id := fmt.Sprintf("%s_%d:%d", rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
// We use ID as key to ensure we have every unique message only once.
// Some IMAP servers responded twice the same message...
messagesInfo[id] = imapMessageInfo{
id: id,
uid: imapMessage.Uid,
@ -173,6 +176,10 @@ func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<-
items := []imap.FetchItem{imap.FetchUid, imap.FetchFlags, section.FetchItem()}
processMessageCallback := func(imapMessage *imap.Message) {
if progress.shouldStop() {
return
}
id, ok := uidToID[imapMessage.Uid]
// Sometimes, server sends not requested messages.
@ -217,3 +224,7 @@ func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Me
Targets: rule.TargetMailboxes,
}
}
func getUniqueMessageID(mailboxName string, uidValidity, uid uint32) string {
return fmt.Sprintf("%s_%d:%d", mailboxName, uidValidity, uid)
}

View File

@ -40,6 +40,10 @@ func (p *MBOXProvider) TransferTo(rules transferRules, progress *Progress, ch ch
return
}
if len(filePathsPerFolder) == 0 {
return
}
for folderName, filePaths := range filePathsPerFolder {
// No error guaranteed by getFilePathsPerFolder.
rule, _ := rules.getRuleBySourceMailboxName(folderName)
@ -50,6 +54,7 @@ func (p *MBOXProvider) TransferTo(rules transferRules, progress *Progress, ch ch
p.updateCount(rule, progress, filePath)
}
}
progress.countsFinal()
for folderName, filePaths := range filePathsPerFolder {
// No error guaranteed by getFilePathsPerFolder.

View File

@ -24,6 +24,7 @@ import (
pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const pmapiListPageSize = 150
@ -59,10 +60,11 @@ func (p *PMAPIProvider) loadCounts(rules transferRules, progress *Progress) {
rule := rule
progress.callWrap(func() error {
_, total, err := p.listMessages(&pmapi.MessagesFilter{
LabelID: rule.SourceMailbox.ID,
Begin: rule.FromTime,
End: rule.ToTime,
Limit: 0,
AddressID: p.addressID,
LabelID: rule.SourceMailbox.ID,
Begin: rule.FromTime,
End: rule.ToTime,
Limit: 0,
})
if err != nil {
log.WithError(err).Warning("Problem to load counts")
@ -72,10 +74,11 @@ func (p *PMAPIProvider) loadCounts(rules transferRules, progress *Progress) {
return nil
})
}
progress.countsFinal()
}
func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Message, skipEncryptedMessages bool) {
nextID := ""
page := 0
for {
if progress.shouldStop() {
break
@ -84,30 +87,33 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
isLastPage := true
progress.callWrap(func() error {
// Would be better to filter by Begin and BeginID to be sure
// in case user deletes messages during the process, no message
// is skipped (paging is off then), but API does not support
// filtering by both mentioned fields at the same time.
desc := false
pmapiMessages, count, err := p.listMessages(&pmapi.MessagesFilter{
pmapiMessages, total, err := p.listMessages(&pmapi.MessagesFilter{
AddressID: p.addressID,
LabelID: rule.SourceMailbox.ID,
Begin: rule.FromTime,
End: rule.ToTime,
BeginID: nextID,
PageSize: pmapiListPageSize,
Page: 0,
Page: page,
Sort: "ID",
Desc: &desc,
})
if err != nil {
return err
}
log.WithField("label", rule.SourceMailbox.ID).WithField("next", nextID).WithField("count", count).Debug("Listing messages")
log.WithFields(logrus.Fields{
"label": rule.SourceMailbox.ID,
"page": page,
"total": total,
"count": len(pmapiMessages),
}).Debug("Listing messages")
isLastPage = len(pmapiMessages) < pmapiListPageSize
// The first ID is the last one from the last page (= do not export twice the same one).
if nextID != "" {
pmapiMessages = pmapiMessages[1:]
}
for _, pmapiMessage := range pmapiMessages {
if progress.shouldStop() {
break
@ -122,9 +128,7 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
}
}
if !isLastPage {
nextID = pmapiMessages[len(pmapiMessages)-1].ID
}
page++
return nil
})

View File

@ -71,6 +71,11 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
log.Info("Started transfer from channel to PMAPI")
defer log.Info("Finished transfer from channel to PMAPI")
// Cache has to be cleared before each transfer to not contain
// old stuff from previous cancelled run.
p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{}
p.importMsgReqSize = 0
for msg := range ch {
if progress.shouldStop() {
break

View File

@ -229,8 +229,8 @@ func (r *transferRules) getRule(sourceMailbox Mailbox) *Rule {
return r.rules[h]
}
// getRules returns all set rules.
func (r *transferRules) getRules() []*Rule {
// getSortedRules returns all set rules in order by `byRuleOrder`.
func (r *transferRules) getSortedRules() []*Rule {
rules := []*Rule{}
for _, rule := range r.rules {
rules = append(rules, rule)

View File

@ -239,7 +239,7 @@ func TestOrderRules(t *testing.T) {
}
gotMailboxNames := []string{}
for _, rule := range transferRules.getRules() {
for _, rule := range transferRules.getSortedRules() {
gotMailboxNames = append(gotMailboxNames, rule.SourceMailbox.Name)
}

View File

@ -34,10 +34,11 @@ type Transfer struct {
panicHandler PanicHandler
metrics MetricsManager
id string
dir string
logDir string
rules transferRules
source SourceProvider
target TargetProvider
rulesCache []*Rule
sourceMboxCache []Mailbox
targetMboxCache []Mailbox
}
@ -47,14 +48,14 @@ type Transfer struct {
// source := transfer.NewEMLProvider(...)
// target := transfer.NewPMAPIProvider(...)
// transfer.New(source, target, ...)
func New(panicHandler PanicHandler, metrics MetricsManager, transferDir string, source SourceProvider, target TargetProvider) (*Transfer, error) {
func New(panicHandler PanicHandler, metrics MetricsManager, logDir, rulesDir string, source SourceProvider, target TargetProvider) (*Transfer, error) {
transferID := fmt.Sprintf("%x", sha256.Sum256([]byte(source.ID()+"-"+target.ID())))
rules := loadRules(transferDir, transferID)
rules := loadRules(rulesDir, transferID)
transfer := &Transfer{
panicHandler: panicHandler,
metrics: metrics,
id: transferID,
dir: transferDir,
logDir: logDir,
rules: rules,
source: source,
target: target,
@ -108,16 +109,19 @@ func (t *Transfer) SetGlobalTimeLimit(fromTime, toTime int64) {
// SetRule sets sourceMailbox for transfer.
func (t *Transfer) SetRule(sourceMailbox Mailbox, targetMailboxes []Mailbox, fromTime, toTime int64) error {
t.rulesCache = nil
return t.rules.setRule(sourceMailbox, targetMailboxes, fromTime, toTime)
}
// UnsetRule unsets sourceMailbox from transfer.
func (t *Transfer) UnsetRule(sourceMailbox Mailbox) {
t.rulesCache = nil
t.rules.unsetRule(sourceMailbox)
}
// ResetRules unsets all rules.
func (t *Transfer) ResetRules() {
t.rulesCache = nil
t.rules.reset()
}
@ -128,7 +132,10 @@ func (t *Transfer) GetRule(sourceMailbox Mailbox) *Rule {
// GetRules returns all set transfer rules.
func (t *Transfer) GetRules() []*Rule {
return t.rules.getRules()
if t.rulesCache == nil {
t.rulesCache = t.rules.getSortedRules()
}
return t.rulesCache
}
// SourceMailboxes returns mailboxes available at source side.
@ -171,7 +178,7 @@ func (t *Transfer) Start() *Progress {
t.metrics.Start()
log := log.WithField("id", t.id)
reportFile := newFileReport(t.dir, t.id)
reportFile := newFileReport(t.logDir, t.id)
progress := newProgress(log, reportFile)
ch := make(chan Message)
@ -179,7 +186,6 @@ func (t *Transfer) Start() *Progress {
go func() {
defer t.panicHandler.HandlePanic()
progress.start()
t.source.TransferTo(t.rules, &progress, ch)
close(ch)
}()

View File

@ -0,0 +1,53 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFo9OeEBEAC+fPrLcUBY+YUc5YiMrYJQ6ogrJWMGC00h9fAv3PsrHkBz0z7c
QFDyNdNatokFDtZDX115M0vzDwk5NkcjmO7CWbf6nCZcwYqOSrBoH8wNT9uTS/6p
R3AHk1r3C/36QG3iWx6Wg4ycRkXWYToT3/yh5waE5BbLi/9TSBAdfJzTyxt4IpZG
3OTMnOwuz6eNRWVHkA48CJydWS6M8z+jIsBwFq4nOIChvLjIF42PuAT1VaiCYSmy
4sU1YxxWof5z9HY0XghRpd7aUIgzAIsXUbaEXh/3iCZDUMN5LwkyAn+r5j3SMNzk
2htF8V7qWE8ldYNVrpeEwyor0x1wMzpbb/C4Y8wXe8rP01d0ApiHVRETzsQk2esf
XuSrBCtpyLc6ET1lluiL2sVUUelAPueUQlOyYXfL2X958i0TgBCi6QRPXxbPjCPs
d1UzLPCSUNUO+/7fslZCax26d1r1kbHzJLAN1Jer6rxoEDaEiVSCUTnHgykCq5rO
C3PScGEdOaIi4H5c6YFZrLmdz409YmJEWLKIPV/u5DpI+YGmAfAevrjkMBgQBOmZ
D8Gp19LnRtmqjVh2rVdr8yc5nAjoNOZwanMwD5vCWPUVELWXubNFBv8hqZMxHZqW
GrB8x8hkdgiNmuyqsxzBmOEJHWLlvbFhvHhIedT8paU/spL/qJmWp3EB4QARAQAB
tExQcm90b24gVGVjaG5vbG9naWVzIEFHIChQcm90b25NYWlsIEJyaWRnZSBkZXZl
bG9wZXJzKSA8YnJpZGdlQHByb3Rvbm1haWwuY2g+iQJUBBMBCAA+AhsDBQsJCAcC
BhUICQoLAgQWAgMBAh4BAheAFiEE1R5k0+Y+3D7veGTO4sddaOYjSwcFAlv377wF
CQO83tsACgkQ4sddaOYjSwfhng//WNhZqr0StuN4KbYdQG+FY+aLijLhiVI3i4j6
wUis+7UWFNMUGePsBUrF7zOrzo4Vp16FSRhhpveIbDMVJg4yGlzwN+jZr9FBvF8z
kbOqjajkTF3rOyqSQCpZVgeamRt6c4gGQTOwfwxB4K5mVg4rv65ISIKjLUtCZ27g
pD6eJs25LhyZQnI65JHpHDkVar7oQ2nbWv0tn2wrrUKBE9hRM5Jn1xGaHYkrYxPe
HNDHrqxJUDbPfJhca54M99bs9Qum3KkT1WWU5/0trA0V8eUZa93zydLNynJJcqbq
KUYBvOnpzL/0l3hdffmolpUXWFrlFPlOLVQlK4Kc6oQqS2KWBySQHg9klTto1p9c
pNZE3sO5+UfleyXW0dN6DcU/xiwoYKJ/+x4JZYtvqH/kP7gve2oznEsLMw6k2QZo
O1GihEpoXpOezs46+ER/YGx4ZF2ne2bmYnzoOOZBbGXwsMZTNaa9QJHbc1bz9jjj
IFBc1zmrdi0nsbjlvLugEYIbSb/WP0wKwG66zTatslRIQ2unlUJNnWb0E4VLgz9y
q57QpvxS7D312dZV0NnAwhyDI+54XAivXTQb0fAGfcgbtKdKpJb1dcAMb9WOBnpr
BK7XLsWbJj5v5nB3AuWer7NhUyJB/ogWQtqRUY1bAcI4cB1zFwYq/PL0sbfAHDxx
ZEF6Xhi5Ag0EWj054QEQALdPQOlRT1omHljxnN64jFuDXXSIb6zqaBvUwdYoDpV2
dfRmzGklsCVA7WHXBmDWbUe9avgO3OO7ANw6/JzzYjP+jwImpJg7cSqTqW8A1U6T
YfGXVUV3a/obIEttl7bI9BsUNgmLsBYIwHov+gl/ajKQdALYHCmq3Bj6o7BBeWPp
Vpk9dzjcsLVbmNszNGP1Ik5dKE0jZUi6h+YoVuJE9o/+T+jxoqFRpXNsZqWOEKmC
HDz6TTs1iTp+CoZ/5g0eKph6XJ+TuNoqF9491IYEFn9oxzsoIBkewTY/fJWmXf++
cnpBODrZLF/GoRFc7MW9Kael9vmQ0J7mjM2bFs308lH0rRrfmdlLAU5iKgPv0akx
nnnUqvCcoekFMURDtP3z09KZXuOMnt834utd7WLe+LZD6dxs+rPhyDiW80E8Bdlz
1Jo+c2g6toIN+uD7/f5gwaZaXhJB0oO7fWSVVo+HJprWBnmf9frgKq1OcS0BNvA+
4Aip2hhFqWJAbUQXCyMaeU2WTWIzy0FQ6SEFFy/RM8O5O1HHsDYjtIic9QJ/PqSD
0qN7LMlkjR8AdWvAxm95i5GpxDZODldsOneeummvsn3I1jCoULTik7iJVdRuY1V3
vfsYAkefGN/n2ga3MvatCJipwoCGsMgUXGTdokXOqKBgMBuBLCkxj2wlol2R9p8R
ABEBAAGJAjwEGAEIACYCGwwWIQTVHmTT5j7cPu94ZM7ix11o5iNLBwUCW/fygQUJ
A7zhoAAKCRDix11o5iNLB7eTD/4x8I7I7MQV63Z8hDShJixSi49bfXeykzlrZyrA
bqNr7JrIKzgX5F1HTU0JF3m+VGkhlpMIlTF/jLq9f1vzmRuiPvux/jItXYbnHFhh
lFekwZkXx4nS5iwjpMDt6C1ERftv+Z5yHK91mZsr6eNcfA6VeIdKBQenltZvDVsq
HSVEsDhhsKJ473tauwuPXks7cqq8tsSgVzHzRO+CV6HV1b3Muiy5ZA73RC1oIGYT
l5zIk1M0h2FIyCfffTBEhZ/dAMErzwcogTA+EAq+OlypTiw2SXZDRx5sQ8T+018k
d3zuJZ4PhzJDpzQ627zhy+1M4HPYOHM/nipOkoGl9D8qrFb/DEcoQ6B4FKVRWugJ
7ZdtBpnrzh9eVmH9Z1LyKvhSHMSF6iklvIxlCGXas5j71kRg/Yc/aH/St9tV0ZIP
1XhwEAY+ul1LCP2YgunCJEJwiG+MZBEZTU5V0gfjdNa/nqNGPOTbLy5oGPV6yWT3
b3mx3wudw+aI8MXXPzMBCAn57S7/xuQ4fODx62NOeme/BOnjASbeE3mZ5/3qBbnu
YIgVTYNp5frIG3wK8W1r6NY2vYQ0iBIzOCIxnNDjYqsGlpAytX+SM+YY7J9n1dZa
UsUfX5Qs+D9VIr/j3jurObPehn9fahCOC2YXicKgSbmQyBLysbFyLT5AMpn5aes0
qdwhrw==
=B6/F
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,102 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"regexp"
"strconv"
"strings"
)
var nonVersionChars = regexp.MustCompile(`([^0-9.]+)`) //nolint[gochecknoglobals]
// sanitizeVersion returns only numbers and periods.
func sanitizeVersion(version string) string {
return nonVersionChars.ReplaceAllString(version, "")
}
// Result can be false positive, but must not be false negative.
// Assuming
// * dot separated integers format e.g. "A.B.C.…" where A,B,C,… are integers
// * `1.1` == `1.1.0` (i.e. first is not newer)
// * `1.1.1` > `1.1` (i.e. first is newer)
func isFirstVersionNewer(first, second string) (firstIsNewer bool, err error) {
first = sanitizeVersion(first)
second = sanitizeVersion(second)
firstIsNewer, err = false, nil
if first == second {
return
}
firstIsNewer = true
var firstArr, secondArr []int
if firstArr, err = versionStrToInts(first); err != nil {
return
}
if secondArr, err = versionStrToInts(second); err != nil {
return
}
verLength := max(len(firstArr), len(secondArr))
firstArr = appendZeros(firstArr, verLength)
secondArr = appendZeros(secondArr, verLength)
for i := 0; i < verLength; i++ {
if firstArr[i] == secondArr[i] {
continue
}
return firstArr[i] > secondArr[i], nil
}
return false, nil
}
func versionStrToInts(version string) (intArr []int, err error) {
strArr := strings.Split(version, ".")
intArr = make([]int, len(strArr))
for index, item := range strArr {
if item == "" {
intArr[index] = 0
continue
}
intArr[index], err = strconv.Atoi(item)
if err != nil {
return
}
}
return
}
func appendZeros(ints []int, newsize int) []int {
size := len(ints)
if size >= newsize {
return ints
}
zeros := make([]int, newsize-size)
return append(ints, zeros...)
}
func max(ints ...int) (max int) {
max = ints[0]
for _, a := range ints {
if max < a {
max = a
}
}
return
}

View File

@ -0,0 +1,78 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"testing"
"github.com/stretchr/testify/require"
)
type testDataValues struct {
expectErr, expectedNewer bool
first, second string
}
type testDataList []testDataValues
func (tdl *testDataList) add(err, newer bool, first, second string) { //nolint[unparam]
*tdl = append(*tdl, testDataValues{err, newer, first, second})
}
func (tdl *testDataList) addFirstIsNewer(first, second string) {
tdl.add(false, true, first, second)
tdl.add(false, false, second, first)
}
func TestCompareVersion(t *testing.T) {
testData := testDataList{}
// same is never newer
testData.add(false, false, "1.1.1", "1.1.1")
testData.add(false, false, "1.1.0", "1.1")
testData.add(false, false, "1.0.0", "1")
testData.add(false, false, ".1.1", "0.1.1")
testData.add(false, false, "0.1.1", ".1.1")
testData.addFirstIsNewer("1.1.10", "1.1.1")
testData.addFirstIsNewer("1.10.1", "1.1.1")
testData.addFirstIsNewer("10.1.1", "1.1.1")
testData.addFirstIsNewer("1.1.1", "0.1.1")
testData.addFirstIsNewer("1.1.1", "1.0.1")
testData.addFirstIsNewer("1.1.1", "1.1.0")
testData.addFirstIsNewer("1.1.1", "1")
testData.addFirstIsNewer("1.1.1", "1.1")
testData.addFirstIsNewer("1.1.1.1", "1.1.1")
testData.addFirstIsNewer("1.1.1 beta", "1.1.0")
testData.addFirstIsNewer("1z.1z.1z", "1.1.0")
testData.addFirstIsNewer("1a.1b.1c", "1.1.0")
for _, td := range testData {
t.Log(td)
isNewer, err := isFirstVersionNewer(td.first, td.second)
if td.expectErr {
require.True(t, err != nil, "expected error but got nil for %#v", td)
require.True(t, true == isNewer, "error expected but first is not newer for %#v", td)
continue
}
require.True(t, err == nil, "expected no error but have %v for %#v", err, td)
require.True(t, isNewer == td.expectedNewer, "expected %v but have %v for %#v", td.expectedNewer, isNewer, err, td)
}
}

View File

@ -0,0 +1,131 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"errors"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/ProtonMail/proton-bridge/pkg/dialer"
)
func mkdirAllClear(path string) error {
if err := os.RemoveAll(path); err != nil {
return err
}
return os.MkdirAll(path, 0750)
}
func downloadToBytes(path string) (out []byte, err error) {
var (
client *http.Client
response *http.Response
)
client = dialer.DialTimeoutClient()
log.WithField("path", path).Trace("Downloading")
response, err = client.Get(path)
if err != nil {
return
}
out, err = ioutil.ReadAll(response.Body)
_ = response.Body.Close()
if response.StatusCode < http.StatusOK || http.StatusIMUsed < response.StatusCode {
err = errors.New(path + " " + response.Status)
}
return
}
func downloadWithProgress(status *Progress, sourceURL, targetPath string) (err error) {
targetFile, err := os.Create(targetPath)
if err != nil {
log.Warnf("Cannot create update file %s: %v", targetPath, err)
return
}
defer targetFile.Close() //nolint[errcheck]
var (
client *http.Client
response *http.Response
)
client = dialer.DialTimeoutClient()
response, err = client.Get(sourceURL)
if err != nil {
return
}
defer response.Body.Close() //nolint[errcheck]
contentLength, _ := strconv.ParseUint(response.Header.Get("Content-Length"), 10, 64)
wc := WriteCounter{
Status: status,
Target: targetFile,
Size: contentLength,
}
err = wc.ReadAll(response.Body)
return
}
func downloadWithSignature(status *Progress, sourceURL, targetDir string) (localPath string, err error) {
localPath = filepath.Join(targetDir, filepath.Base(sourceURL))
if err = downloadWithProgress(nil, sourceURL+sigExtension, localPath+sigExtension); err != nil {
return
}
if err = downloadWithProgress(status, sourceURL, localPath); err != nil {
return
}
return
}
type WriteCounter struct {
Status *Progress
Target io.Writer
processed, Size, counter uint64
}
func (s *WriteCounter) ReadAll(source io.Reader) (err error) {
s.counter = uint64(0)
if s.Target == nil {
return errors.New("can not read all, target unset")
}
if source == nil {
return errors.New("can not read all, source unset")
}
_, err = io.Copy(s.Target, io.TeeReader(source, s))
return
}
func (s *WriteCounter) Write(p []byte) (int, error) {
if s.Status != nil && s.Size != 0 {
s.processed += uint64(len(p))
fraction := float32(s.processed) / float32(s.Size)
if s.counter%uint64(100) == 0 || fraction == 1. {
s.Status.UpdateProcessed(fraction)
}
}
s.counter++
return len(p), nil
}

View File

@ -0,0 +1,50 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
const (
InfoCurrentVersion = 1 + iota
InfoDownloading
InfoVerifying
InfoUnpacking
InfoUpgrading
InfoQuitApp
InfoRestartApp
)
type Progress struct {
Processed float32 // fraction of finished procedure [0.0-1.0]
Description int // description by code (needs to be translated anyway)
Err error // occurred error
channel chan<- Progress
}
func (s *Progress) Update() {
s.channel <- *s
}
func (s *Progress) UpdateDescription(description int) {
s.Description = description
s.Processed = 0
s.Update()
}
func (s *Progress) UpdateProcessed(processed float32) {
s.Processed = processed
s.Update()
}

View File

@ -0,0 +1,108 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"bytes"
"encoding/hex"
"errors"
"io"
"os"
"os/exec"
"runtime"
"golang.org/x/crypto/openpgp"
)
// gpg --export D51E64D3E63EDC3EEF7864CEE2C75D68E6234B07 | xxd -p | tr -d '\n' | xclip
const (
keyID = "D51E64D3E63EDC3EEF7864CEE2C75D68E6234B07"
pubkeyHex = "99020d045a3d39e1011000be7cfacb714058f9851ce5888cad8250ea882b2563060b4d21f5f02fdcfb2b1e4073d33edc4050f235d35ab689050ed6435f5d79334bf30f093936472398eec259b7fa9c265cc18a8e4ab0681fcc0d4fdb934bfea9477007935af70bfdfa406de25b1e96838c9c4645d6613a13dffca1e70684e416cb8bff5348101d7c9cd3cb1b78229646dce4cc9cec2ecfa78d456547900e3c089c9d592e8cf33fa322c07016ae273880a1bcb8c8178d8fb804f555a8826129b2e2c535631c56a1fe73f476345e0851a5deda508833008b1751b6845e1ff788264350c3792f0932027fabe63dd230dce4da1b45f15eea584f25758355ae9784c32a2bd31d70333a5b6ff0b863cc177bcacfd35774029887551113cec424d9eb1f5ee4ab042b69c8b73a113d6596e88bdac55451e9403ee7944253b26177cbd97f79f22d138010a2e9044f5f16cf8c23ec7755332cf09250d50efbfedfb256426b1dba775af591b1f324b00dd497abeabc681036848954825139c7832902ab9ace0b73d270611d39a222e07e5ce98159acb99dcf8d3d62624458b2883d5feee43a48f981a601f01ebeb8e430181004e9990fc1a9d7d2e746d9aa8d5876ad576bf327399c08e834e6706a73300f9bc258f51510b597b9b34506ff21a993311d9a961ab07cc7c86476088d9aecaab31cc198e1091d62e5bdb161bc784879d4fca5a53fb292ffa89996a77101e10011010001b44c50726f746f6e20546563686e6f6c6f67696573204147202850726f746f6e4d61696c2042726964676520646576656c6f7065727329203c6272696467654070726f746f6e6d61696c2e63683e89025404130108003e021b03050b09080702061508090a0b020416020301021e01021780162104d51e64d3e63edc3eef7864cee2c75d68e6234b0705025bf7efbc050903bcdedb000a0910e2c75d68e6234b07e19e0fff58d859aabd12b6e37829b61d406f8563e68b8a32e18952378b88fac148acfbb51614d31419e3ec054ac5ef33abce8e15a75e85491861a6f7886c3315260e321a5cf037e8d9afd141bc5f3391b3aa8da8e44c5deb3b2a92402a5956079a991b7a7388064133b07f0c41e0ae66560e2bbfae484882a32d4b42676ee0a43e9e26cdb92e1c9942723ae491e91c39156abee84369db5afd2d9f6c2bad428113d851339267d7119a1d892b6313de1cd0c7aeac495036cf7c985c6b9e0cf7d6ecf50ba6dca913d56594e7fd2dac0d15f1e5196bddf3c9d2cdca724972a6ea294601bce9e9ccbff497785d7df9a8969517585ae514f94e2d54252b829cea842a4b62960724901e0f64953b68d69f5ca4d644dec3b9f947e57b25d6d1d37a0dc53fc62c2860a27ffb1e09658b6fa87fe43fb82f7b6a339c4b0b330ea4d906683b51a2844a685e939ecece3af8447f606c78645da77b66e6627ce838e6416c65f0b0c65335a6bd4091db7356f3f638e320505cd739ab762d27b1b8e5bcbba011821b49bfd63f4c0ac06ebacd36adb25448436ba795424d9d66f413854b833f72ab9ed0a6fc52ec3df5d9d655d0d9c0c21c8323ee785c08af5d341bd1f0067dc81bb4a74aa496f575c00c6fd58e067a6b04aed72ec59b263e6fe6707702e59eafb361532241fe881642da91518d5b01c238701d7317062afcf2f4b1b7c01c3c7164417a5e18b9020d045a3d39e1011000b74f40e9514f5a261e58f19cdeb88c5b835d74886facea681bd4c1d6280e957675f466cc6925b02540ed61d70660d66d47bd6af80edce3bb00dc3afc9cf36233fe8f0226a4983b712a93a96f00d54e9361f1975545776bfa1b204b6d97b6c8f41b1436098bb01608c07a2ffa097f6a32907402d81c29aadc18faa3b0417963e956993d7738dcb0b55b98db333463f5224e5d284d236548ba87e62856e244f68ffe4fe8f1a2a151a5736c66a58e10a9821c3cfa4d3b35893a7e0a867fe60d1e2a987a5c9f93b8da2a17de3dd48604167f68c73b2820191ec1363f7c95a65dffbe727a41383ad92c5fc6a1115cecc5bd29a7a5f6f990d09ee68ccd9b16cdf4f251f4ad1adf99d94b014e622a03efd1a9319e79d4aaf09ca1e905314443b4fdf3d3d2995ee38c9edf37e2eb5ded62def8b643e9dc6cfab3e1c83896f3413c05d973d49a3e73683ab6820dfae0fbfdfe60c1a65a5e1241d283bb7d6495568f87269ad606799ff5fae02aad4e712d0136f03ee008a9da1845a962406d44170b231a794d964d6233cb4150e92105172fd133c3b93b51c7b03623b4889cf5027f3ea483d2a37b2cc9648d1f00756bc0c66f798b91a9c4364e0e576c3a779eba69afb27dc8d630a850b4e293b88955d46e635577bdfb1802479f18dfe7da06b732f6ad0898a9c28086b0c8145c64dda245cea8a060301b812c29318f6c25a25d91f69f11001101000189023c041801080026021b0c162104d51e64d3e63edc3eef7864cee2c75d68e6234b0705025bf7f281050903bce1a0000a0910e2c75d68e6234b07b7930ffe31f08ec8ecc415eb767c8434a1262c528b8f5b7d77b293396b672ac06ea36bec9ac82b3817e45d474d4d091779be54692196930895317f8cbabd7f5bf3991ba23efbb1fe322d5d86e71c58619457a4c19917c789d2e62c23a4c0ede82d4445fb6ff99e721caf75999b2be9e35c7c0e9578874a0507a796d66f0d5b2a1d2544b03861b0a278ef7b5abb0b8f5e4b3b72aabcb6c4a05731f344ef8257a1d5d5bdccba2cb9640ef7442d68206613979cc8935334876148c827df7d3044859fdd00c12bcf072881303e100abe3a5ca94e2c36497643471e6c43c4fed35f24777cee259e0f873243a7343adbbce1cbed4ce073d838733f9e2a4e9281a5f43f2aac56ff0c472843a07814a5515ae809ed976d0699ebce1f5e5661fd6752f22af8521cc485ea2925bc8c650865dab398fbd64460fd873f687fd2b7db55d1920fd5787010063eba5d4b08fd9882e9c2244270886f8c6411194d4e55d207e374d6bf9ea3463ce4db2f2e6818f57ac964f76f79b1df0b9dc3e688f0c5d73f33010809f9ed2effc6e4387ce0f1eb634e7a67bf04e9e30126de137999e7fdea05b9ee6088154d8369e5fac81b7c0af16d6be8d636bd84348812333822319cd0e362ab06969032b57f9233e618ec9f67d5d65a52c51f5f942cf83f5522bfe3de3bab39b3de867f5f6a108e0b661789c2a049b990c812f2b1b1722d3e403299f969eb34a9dc21af"
)
var (
pubkeyRing = openpgp.EntityList{} //nolint[gochecknoglobals]
)
func singAndVerify(pathToFile string) (err error) {
err = signFile(pathToFile)
if err != nil {
err = verifyFile(pathToFile)
}
return
}
func signFile(pathToFile string) (err error) {
if runtime.GOOS != "linux" { //nolint[goconst]
return errors.New("tar not implemented only for linux")
}
// assuming gpg detach-sign creates file with suffix .sig by default.
// Lstat does not follow the link i.e. only link is deleted (not link target).
if _, err := os.Lstat(pathToFile + sigExtension); !os.IsNotExist(err) {
_ = os.Remove(pathToFile + sigExtension)
}
cmd := exec.Command("gpg", "--local-user", keyID, "--detach-sign", pathToFile) //nolint[gosec]
return cmd.Run()
}
func verifyFile(pathToFile string) error {
fileReader, err := os.Open(pathToFile) //nolint[gosec]
if err != nil {
return err
}
defer fileReader.Close() //nolint[errcheck]
signatureReader, err := os.Open(pathToFile + sigExtension) //nolint[gosec]
if err != nil {
return err
}
defer signatureReader.Close() //nolint[errcheck]
return verifyBytes(fileReader, signatureReader)
}
func verifyBytes(fileReader, signatureReader io.Reader) (err error) {
if _, err = getPubKey(); err != nil {
return err
}
_, err = openpgp.CheckDetachedSignature(pubkeyRing, fileReader, signatureReader, nil)
/*
if err != nil {
return err
}
if signer == nil || signer.PrimaryKey.KeyId != keyID {
return errors.New("Signer with wrong key ID")
}
*/
return
}
// from opengpg/read_test.go
func getPubKey() (el openpgp.EntityList, err error) {
if pubkeyRing != nil && len(pubkeyRing) != 0 {
return pubkeyRing, nil
}
data, err := hex.DecodeString(pubkeyHex)
if err != nil {
return
}
pubkeyRing, err = openpgp.ReadKeyRing(bytes.NewBuffer(data))
return pubkeyRing, err
}

239
internal/updates/sync.go Normal file
View File

@ -0,0 +1,239 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"crypto/sha256"
"errors"
"io"
"os"
"path/filepath"
)
func syncFolders(localPath, updatePath string) (err error) {
backupDir := filepath.Join(filepath.Dir(updatePath), "backup")
if err = createBackup(localPath, backupDir); err != nil {
return
}
if err = removeMissing(localPath, updatePath); err != nil {
restoreFromBackup(backupDir, localPath)
return
}
if err = copyRecursively(updatePath, localPath); err != nil {
restoreFromBackup(backupDir, localPath)
return
}
return nil
}
func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
log.Debug("remove missing")
// Create list of files.
existingRelPaths := map[string]bool{}
err = filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
relPath, walkErr := filepath.Rel(itemsToKeepPath, keepThis)
if walkErr != nil {
return walkErr
}
log.Debug("path to keep ", relPath)
existingRelPaths[relPath] = true
return nil
})
if err != nil {
return
}
delList := []string{}
err = filepath.Walk(folderToCleanPath, func(removeThis string, _ os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
relPath, walkErr := filepath.Rel(folderToCleanPath, removeThis)
if walkErr != nil {
return walkErr
}
log.Debug("check path ", relPath)
if !existingRelPaths[relPath] {
log.Debug("path not in list, removing ", removeThis)
delList = append(delList, removeThis)
}
return nil
})
if err != nil {
return
}
for _, removeThis := range delList {
if err = os.RemoveAll(removeThis); err != nil && !os.IsNotExist(err) {
log.Error("remove error ", err)
return
}
}
return nil
}
func restoreFromBackup(backupDir, localPath string) {
log.Error("recovering from ", backupDir, " to ", localPath)
_ = copyRecursively(backupDir, localPath)
}
func createBackup(srcFile, dstDir string) (err error) {
log.Debug("backup ", srcFile, " in ", dstDir)
if err = mkdirAllClear(dstDir); err != nil {
return
}
return copyRecursively(srcFile, dstDir)
}
// checksum assumes the file is a regular file and that it exists.
func checksum(path string) (hash string) {
file, err := os.Open(path) //nolint[gosec]
if err != nil {
return
}
defer file.Close() //nolint[errcheck]
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return
}
return string(hasher.Sum(nil))
}
// srcDir including app folder.
// dstDir including app folder.
func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
return filepath.Walk(srcDir, func(srcPath string, srcInfo os.FileInfo, err error) error {
if err != nil {
return err
}
srcIsLink := srcInfo.Mode()&os.ModeSymlink == os.ModeSymlink
srcIsDir := srcInfo.IsDir()
// Non regular source (e.g. named pipes, sockets, devices...).
if !srcIsLink && !srcIsDir && !srcInfo.Mode().IsRegular() {
log.Error("File ", srcPath, " with mode ", srcInfo.Mode())
return errors.New("irregular source file. Copy not implemented")
}
// Destination path.
srcRelPath, err := filepath.Rel(srcDir, srcPath)
if err != nil {
return err
}
dstPath := filepath.Join(dstDir, srcRelPath)
log.Debug("src: ", srcPath, " dst: ", dstPath)
// Destination exists.
dstInfo, err := os.Lstat(dstPath)
if err == nil {
dstIsLink := dstInfo.Mode()&os.ModeSymlink == os.ModeSymlink
dstIsDir := dstInfo.IsDir()
// Non regular destination (e.g. named pipes, sockets, devices...).
if !dstIsLink && !dstIsDir && !dstInfo.Mode().IsRegular() {
log.Error("File ", dstPath, " with mode ", dstInfo.Mode())
return errors.New("irregular target file. Copy not implemented")
}
if dstIsLink {
if err = os.Remove(dstPath); err != nil {
return err
}
}
if !dstIsLink && dstIsDir && !srcIsDir {
if err = os.RemoveAll(dstPath); err != nil {
return err
}
}
// NOTE: Do not return if !dstIsLink && dstIsDir && srcIsDir: the permissions might change.
if dstInfo.Mode().IsRegular() && !srcInfo.Mode().IsRegular() {
if err = os.Remove(dstPath); err != nil {
return err
}
}
} else if !os.IsNotExist(err) {
return err
}
// Create symbolic link and return.
if srcIsLink {
log.Debug("It is a symlink")
linkPath, err := os.Readlink(srcPath)
if err != nil {
return err
}
log.Debug("link to ", linkPath)
return os.Symlink(linkPath, dstPath)
}
// Create dir and return.
if srcIsDir {
log.Debug("It is a dir")
return os.MkdirAll(dstPath, srcInfo.Mode())
}
// Regular files only.
// If files are same return.
if os.SameFile(srcInfo, dstInfo) || checksum(srcPath) == checksum(dstPath) {
log.Debug("Same files, skip copy")
return nil
}
// Create/overwrite regular file.
srcReader, err := os.Open(srcPath) //nolint[gosec]
if err != nil {
return err
}
defer srcReader.Close() //nolint[errcheck]
return copyToTmpFileRename(srcReader, dstPath, srcInfo.Mode())
})
}
func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
log.Debug("Tmp and rename ", dstPath)
tmpPath := dstPath + ".tmp"
if err := copyToFileTruncate(srcReader, tmpPath, dstMode); err != nil {
return err
}
return os.Rename(tmpPath, dstPath)
}
func copyToFileTruncate(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
log.Debug("Copy and truncate ", dstPath)
dstWriter, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, dstMode)
if err != nil {
return err
}
defer dstWriter.Close() //nolint[errcheck]
_, err = io.Copy(dstWriter, srcReader)
return err
}

View File

@ -0,0 +1,157 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
const (
FileType = "File"
SymlinkType = "Symlink"
DirType = "Dir"
EmptyType = "Empty"
NewType = "New"
)
func TestSyncFolder(t *testing.T) {
for _, srcType := range []string{EmptyType, FileType, SymlinkType, DirType} {
for _, dstType := range []string{EmptyType, FileType, SymlinkType, DirType} {
require.NoError(t, checkCopyWorks(srcType, dstType))
log.Warn("OK: from ", srcType, " to ", dstType)
}
}
}
func checkCopyWorks(srcType, dstType string) error {
dirName := "from_" + srcType + "_to_" + dstType
AppCacheDir := "/tmp"
srcDir := filepath.Join(AppCacheDir, "sync_src", dirName)
destDir := filepath.Join(AppCacheDir, "sync_dst", dirName)
// clear before
log.Info("remove all ", srcDir)
err := os.RemoveAll(srcDir)
if err != nil {
return err
}
log.Info("remove all ", destDir)
err = os.RemoveAll(destDir)
if err != nil {
return err
}
// create
err = createTestFolder(srcDir, srcType)
if err != nil {
return err
}
err = createTestFolder(destDir, dstType)
if err != nil {
return err
}
// copy
log.Info("Sync from ", srcDir, " to ", destDir)
err = syncFolders(destDir, srcDir)
if err != nil {
return err
}
// Check
log.Info("check ", srcDir, " and ", destDir)
err = checkThatFilesAreSame(srcDir, destDir)
if err != nil {
return err
}
// clear after
log.Info("remove all ", srcDir)
err = os.RemoveAll(srcDir)
if err != nil {
return err
}
log.Info("remove all ", destDir)
err = os.RemoveAll(destDir)
if err != nil {
return err
}
return err
}
func checkThatFilesAreSame(src, dst string) error {
cmd := exec.Command("diff", "-qr", src, dst) //nolint[gosec]
cmd.Stderr = log.WriterLevel(logrus.ErrorLevel)
cmd.Stdout = log.WriterLevel(logrus.InfoLevel)
return cmd.Run()
}
func createTestFolder(dirPath, dirType string) error {
log.Info("creating folder ", dirPath, " type ", dirType)
if dirType == NewType {
return nil
}
err := mkdirAllClear(dirPath)
if err != nil {
return err
}
if dirType == EmptyType {
return nil
}
path := filepath.Join(dirPath, "testpath")
switch dirType {
case FileType:
err = ioutil.WriteFile(path, []byte("This is a test"), 0640)
if err != nil {
return err
}
case SymlinkType:
err = os.Symlink("../../", path)
if err != nil {
return err
}
case DirType:
err = os.MkdirAll(path, 0750)
if err != nil {
return err
}
err = ioutil.WriteFile(filepath.Join(path, "another_file"), []byte("This is a test"), 0640)
if err != nil {
return err
}
}
return nil
}

126
internal/updates/tar.go Normal file
View File

@ -0,0 +1,126 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"archive/tar"
"compress/gzip"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/sirupsen/logrus"
)
func createTar(tarPath, sourcePath string) error { //nolint[unused]
if runtime.GOOS != "linux" {
return errors.New("tar not implemented only for linux")
}
// Check whether it exists and is a directory.
if _, err := os.Lstat(sourcePath); err != nil {
return err
}
absPath, err := filepath.Abs(tarPath)
if err != nil {
return err
}
cmd := exec.Command("tar", "-zvcf", absPath, filepath.Base(sourcePath)) //nolint[gosec]
cmd.Dir = filepath.Dir(sourcePath)
cmd.Stderr = log.WriterLevel(logrus.ErrorLevel)
cmd.Stdout = log.WriterLevel(logrus.InfoLevel)
return cmd.Run()
}
func untarToDir(tarPath, targetDir string, status *Progress) error { //nolint[funlen]
// Check whether it exists and is a directory.
if ls, err := os.Lstat(targetDir); err == nil {
if !ls.IsDir() {
return errors.New("not a dir")
}
} else {
return err
}
tgzReader, err := os.Open(tarPath) //nolint[gosec]
if err != nil {
return err
}
defer tgzReader.Close() //nolint[errcheck]
size := uint64(0)
if info, err := tgzReader.Stat(); err == nil {
size = uint64(info.Size())
}
wc := &WriteCounter{
Status: status,
Size: size,
}
tarReader, err := gzip.NewReader(io.TeeReader(tgzReader, wc))
if err != nil {
return err
}
fileReader := tar.NewReader(tarReader)
for {
header, err := fileReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if header == nil {
continue
}
targetFile := filepath.Join(targetDir, header.Name)
info := header.FileInfo()
// Create symlink.
if header.Typeflag == tar.TypeSymlink {
if header.Linkname == "" {
return errors.New("missing linkname")
}
if err := os.Symlink(header.Linkname, targetFile); err != nil {
return err
}
continue
}
// Handle case that it is a directory.
if info.IsDir() {
if err := os.MkdirAll(targetFile, info.Mode()); err != nil {
return err
}
continue
}
// Handle case that it is a regular file.
if err := copyToFileTruncate(fileReader, targetFile, info.Mode()); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1 @@
{"Version":"1.1.6","ReleaseDate":"10 Jul 19 11:02 +0200","ReleaseNotes":"• Necessary updates reflecting API changes\n• Report wrongly formated messages\n","ReleaseFixedBugs":"• Fixed verification for contacts signed by older or missing key\n• Outlook always shows attachment icon\n","FixedBugs":["• Fixed verification for contacts signed by older or missing key","• Outlook always shows attachment icon",""],"URL":"https://protonmail.com/download/Bridge-Installer.sh","LandingPage":"https://protonmail.com/bridge/download","UpdateFile":"https://protonmail.com/download/bridge_upgrade_linux.tgz","InstallerFile":"https://protonmail.com/download/Bridge-Installer.sh","DebFile":"https://protonmail.com/download/protonmail-bridge_1.1.6-1_amd64.deb","RpmFile":"https://protonmail.com/download/protonmail-bridge-1.1.6-1.x86_64.rpm","PkgFile":"https://protonmail.com/download/PKGBUILD"}

Binary file not shown.

330
internal/updates/updates.go Normal file
View File

@ -0,0 +1,330 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/kardianos/osext"
"github.com/sirupsen/logrus"
)
const (
sigExtension = ".sig"
)
var (
Host = "https://protonmail.com" //nolint[gochecknoglobals]
DownloadPath = "download" //nolint[gochecknoglobals]
// BuildType specifies type of build (e.g. QA or beta).
BuildType = "" //nolint[gochecknoglobals]
)
var (
log = logrus.WithField("pkg", "bridgeUtils/updates") //nolint[gochecknoglobals]
installFileSuffix = map[string]string{ //nolint[gochecknoglobals]
"darwin": ".dmg",
"windows": ".exe",
"linux": ".sh",
}
ErrDownloadFailed = errors.New("error happened during download") //nolint[gochecknoglobals]
ErrUpdateVerifyFailed = errors.New("cannot verify signature") //nolint[gochecknoglobals]
)
type Updates struct {
version string
revision string
buildTime string
releaseNotes string
releaseFixedBugs string
updateTempDir string
landingPagePath string // Based on Host/; default landing page for download.
installerFileBaseName string // File for initial install or manual reinstall. per goos [exe, dmg, sh].
versionFileBaseName string // Text file containing information about current file. per goos [_linux,_darwin,_windows].json (have .sig file).
updateFileBaseName string // File for automatic update. per goos [_linux,_darwin,_windows].tgz (have .sig file).
linuxFileBaseName string // Prefix of linux package names.
macAppBundleName string // Name of Mac app file in the bundle for update procedure.
cachedNewerVersion *VersionInfo // To have info about latest version even when the internet connection drops.
}
// NewBridge inits Updates struct for bridge.
func NewBridge(updateTempDir string) *Updates {
return &Updates{
version: constants.Version,
revision: constants.Revision,
buildTime: constants.BuildTime,
releaseNotes: bridge.ReleaseNotes,
releaseFixedBugs: bridge.ReleaseFixedBugs,
updateTempDir: updateTempDir,
landingPagePath: "bridge/download",
installerFileBaseName: "Bridge-Installer",
versionFileBaseName: "current_version",
updateFileBaseName: "bridge_upgrade",
linuxFileBaseName: "protonmail-bridge",
macAppBundleName: "ProtonMail Bridge.app",
}
}
// NewImportExport inits Updates struct for import-export.
func NewImportExport(updateTempDir string) *Updates {
return &Updates{
version: constants.Version,
revision: constants.Revision,
buildTime: constants.BuildTime,
releaseNotes: importexport.ReleaseNotes,
releaseFixedBugs: importexport.ReleaseFixedBugs,
updateTempDir: updateTempDir,
landingPagePath: "import-export",
installerFileBaseName: "Import-Export-Installer",
versionFileBaseName: "current_version_ie",
updateFileBaseName: "ie_upgrade",
linuxFileBaseName: "protonmail-import-export",
macAppBundleName: "ProtonMail Import-Export.app",
}
}
func (u *Updates) CreateJSONAndSign(deployDir, goos string) error {
versionInfo := u.getLocalVersion(goos)
versionInfo.Version = sanitizeVersion(versionInfo.Version)
versionFileName := filepath.Base(u.versionFileURL(goos))
versionFilePath := filepath.Join(deployDir, versionFileName)
txt, err := json.Marshal(versionInfo)
if err != nil {
return err
}
if err = ioutil.WriteFile(versionFilePath, txt, 0600); err != nil {
return err
}
if err := singAndVerify(versionFilePath); err != nil {
return err
}
updateFileName := filepath.Base(versionInfo.UpdateFile)
updateFilePath := filepath.Join(deployDir, updateFileName)
if err := singAndVerify(updateFilePath); err != nil {
return err
}
return nil
}
func (u *Updates) CheckIsUpToDate() (isUpToDate bool, latestVersion VersionInfo, err error) {
localVersion := u.GetLocalVersion()
latestVersion, err = u.getLatestVersion()
if err != nil {
return
}
localIsOld, err := isFirstVersionNewer(latestVersion.Version, localVersion.Version)
return !localIsOld, latestVersion, err
}
func (u *Updates) GetDownloadLink() string {
latestVersion, err := u.getLatestVersion()
if err != nil || latestVersion.InstallerFile == "" {
localVersion := u.GetLocalVersion()
return localVersion.GetDownloadLink()
}
return latestVersion.GetDownloadLink()
}
func (u *Updates) GetLocalVersion() VersionInfo {
return u.getLocalVersion(runtime.GOOS)
}
func (u *Updates) getLocalVersion(goos string) VersionInfo {
version := u.version
if BuildType != "" {
version += " " + BuildType
}
versionInfo := VersionInfo{
Version: version,
Revision: u.revision,
ReleaseDate: u.buildTime,
ReleaseNotes: u.releaseNotes,
ReleaseFixedBugs: u.releaseFixedBugs,
FixedBugs: strings.Split(u.releaseFixedBugs, "\n"),
URL: u.installerFileURL(goos),
LandingPage: u.landingPageURL(),
UpdateFile: u.updateFileURL(goos),
InstallerFile: u.installerFileURL(goos),
}
if goos == "linux" {
pkgName := u.linuxFileBaseName
pkgRel := "1"
pkgBase := strings.Join([]string{Host, DownloadPath, pkgName}, "/")
versionInfo.DebFile = pkgBase + "_" + u.version + "-" + pkgRel + "_amd64.deb"
versionInfo.RpmFile = pkgBase + "-" + u.version + "-" + pkgRel + ".x86_64.rpm"
versionInfo.PkgFile = strings.Join([]string{Host, DownloadPath, "PKGBUILD"}, "/")
}
return versionInfo
}
func (u *Updates) getLatestVersion() (latestVersion VersionInfo, err error) {
version, err := downloadToBytes(u.versionFileURL(runtime.GOOS))
if err != nil {
if u.cachedNewerVersion != nil {
return *u.cachedNewerVersion, nil
}
return
}
signature, err := downloadToBytes(u.signatureFileURL(runtime.GOOS))
if err != nil {
if u.cachedNewerVersion != nil {
return *u.cachedNewerVersion, nil
}
return
}
if err = verifyBytes(bytes.NewReader(version), bytes.NewReader(signature)); err != nil {
return
}
if err = json.NewDecoder(bytes.NewReader(version)).Decode(&latestVersion); err != nil {
return
}
if localIsOld, _ := isFirstVersionNewer(latestVersion.Version, u.version); localIsOld {
u.cachedNewerVersion = &latestVersion
}
return
}
func (u *Updates) landingPageURL() string {
return strings.Join([]string{Host, u.landingPagePath}, "/")
}
func (u *Updates) signatureFileURL(goos string) string {
return u.versionFileURL(goos) + sigExtension
}
func (u *Updates) versionFileURL(goos string) string {
return strings.Join([]string{Host, DownloadPath, u.versionFileBaseName + "_" + goos + ".json"}, "/")
}
func (u *Updates) installerFileURL(goos string) string {
return strings.Join([]string{Host, DownloadPath, u.installerFileBaseName + installFileSuffix[goos]}, "/")
}
func (u *Updates) updateFileURL(goos string) string {
return strings.Join([]string{Host, DownloadPath, u.updateFileBaseName + "_" + goos + ".tgz"}, "/")
}
func (u *Updates) StartUpgrade(currentStatus chan<- Progress) { // nolint[funlen]
status := &Progress{channel: currentStatus}
defer status.Update()
// Get latest version.
var verInfo VersionInfo
status.UpdateDescription(InfoCurrentVersion)
if verInfo, status.Err = u.getLatestVersion(); status.Err != nil {
return
}
if verInfo.UpdateFile == "" {
log.Warn("Empty update URL. Update manually.")
status.Err = ErrDownloadFailed
return
}
// Download.
status.UpdateDescription(InfoDownloading)
if status.Err = mkdirAllClear(u.updateTempDir); status.Err != nil {
return
}
var updateTar string
updateTar, status.Err = downloadWithSignature(
status,
verInfo.UpdateFile,
u.updateTempDir,
)
if status.Err != nil {
return
}
// Check signature.
status.UpdateDescription(InfoVerifying)
status.Err = verifyFile(updateTar)
if status.Err != nil {
log.Warnf("Cannot verify update file %s: %v", updateTar, status.Err)
status.Err = ErrUpdateVerifyFailed
return
}
// Untar.
status.UpdateDescription(InfoUnpacking)
status.Err = untarToDir(updateTar, u.updateTempDir, status)
if status.Err != nil {
return
}
// Run upgrade (OS specific).
status.UpdateDescription(InfoUpgrading)
switch runtime.GOOS {
case "windows": //nolint[goconst]
cmd := exec.Command("./" + u.installerFileBaseName) // nolint[gosec]
cmd.Dir = u.updateTempDir
status.Err = cmd.Start()
case "darwin":
// current path is better then appDir = filepath.Join("/Applications")
var exePath string
exePath, status.Err = osext.Executable()
if status.Err != nil {
return
}
localPath := filepath.Dir(exePath) // Macos
localPath = filepath.Dir(localPath) // Contents
localPath = filepath.Dir(localPath) // .app
updatePath := filepath.Join(u.updateTempDir, u.macAppBundleName)
log.Warn("localPath ", localPath)
log.Warn("updatePath ", updatePath)
status.Err = syncFolders(localPath, updatePath)
if status.Err != nil {
return
}
status.UpdateDescription(InfoRestartApp)
return
default:
status.Err = errors.New("upgrade for " + runtime.GOOS + " not implemented")
}
status.UpdateDescription(InfoQuitApp)
}

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 build_beta
package updates
func init() {
DownloadPath = "download/beta"
BuildType = "beta"
}

View File

@ -0,0 +1,26 @@
// 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 build_qa
package updates
func init() {
Host = "https://bridgeteam.protontech.ch"
DownloadPath = "download/qa"
BuildType = "QA"
}

View File

@ -0,0 +1,184 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"io/ioutil"
"net/http"
"os"
"runtime"
"testing"
"github.com/stretchr/testify/require"
)
const testServerPort = "8999"
var testUpdateDir string //nolint[gochecknoglobals]
func TestMain(m *testing.M) {
setup()
code := m.Run()
shutdown()
os.Exit(code)
}
func setup() {
var err error
testUpdateDir, err = ioutil.TempDir("", "upgrade")
if err != nil {
panic(err)
}
Host = "http://localhost:" + testServerPort
go startServer()
}
func shutdown() {
_ = os.RemoveAll(testUpdateDir)
}
func startServer() {
http.HandleFunc("/download/current_version_linux.json", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./testdata/current_version_linux.json")
})
http.HandleFunc("/download/current_version_linux.json.sig", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./testdata/current_version_linux.json.sig")
})
http.HandleFunc("/download/current_version_darwin.json", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./testdata/current_version_linux.json")
})
http.HandleFunc("/download/current_version_darwin.json.sig", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./testdata/current_version_linux.json.sig")
})
panic(http.ListenAndServe(":"+testServerPort, nil))
}
func TestCheckBridgeIsUpToDate(t *testing.T) {
updates := newTestUpdates("1.1.6")
isUpToDate, _, err := updates.CheckIsUpToDate()
require.NoError(t, err)
require.True(t, isUpToDate, "Bridge should be up to date")
}
func TestCheckBridgeIsNotUpToDate(t *testing.T) {
updates := newTestUpdates("1.1.5")
isUpToDate, _, err := updates.CheckIsUpToDate()
require.NoError(t, err)
require.True(t, !isUpToDate, "Bridge should not be up to date")
}
func TestGetLocalVersion(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test because local version for windows is currently not supported by tests.")
}
updates := newTestUpdates("1")
expectedVersion := VersionInfo{
Version: "1",
Revision: "rev123",
ReleaseDate: "42",
ReleaseNotes: "• new feature",
ReleaseFixedBugs: "• fixed foo",
FixedBugs: []string{"• fixed foo"},
URL: Host + "/" + DownloadPath + "/Bridge-Installer.sh",
LandingPage: Host + "/bridge/download",
UpdateFile: Host + "/" + DownloadPath + "/bridge_upgrade_linux.tgz",
InstallerFile: Host + "/" + DownloadPath + "/Bridge-Installer.sh",
DebFile: Host + "/" + DownloadPath + "/protonmail-bridge_1-1_amd64.deb",
RpmFile: Host + "/" + DownloadPath + "/protonmail-bridge-1-1.x86_64.rpm",
PkgFile: Host + "/" + DownloadPath + "/PKGBUILD",
}
if runtime.GOOS == "darwin" {
expectedVersion.URL = Host + "/" + DownloadPath + "/Bridge-Installer.dmg"
expectedVersion.UpdateFile = Host + "/" + DownloadPath + "/bridge_upgrade_darwin.tgz"
expectedVersion.InstallerFile = expectedVersion.URL
expectedVersion.DebFile = ""
expectedVersion.RpmFile = ""
expectedVersion.PkgFile = ""
}
version := updates.GetLocalVersion()
require.Equal(t, expectedVersion, version)
}
func TestGetLatestVersion(t *testing.T) {
updates := newTestUpdates("1")
expectedVersion := VersionInfo{
Version: "1.1.6",
Revision: "",
ReleaseDate: "10 Jul 19 11:02 +0200",
ReleaseNotes: "• Necessary updates reflecting API changes\n• Report wrongly formated messages\n",
ReleaseFixedBugs: "• Fixed verification for contacts signed by older or missing key\n• Outlook always shows attachment icon\n",
FixedBugs: []string{
"• Fixed verification for contacts signed by older or missing key",
"• Outlook always shows attachment icon",
"",
},
URL: "https://protonmail.com/download/Bridge-Installer.sh",
LandingPage: "https://protonmail.com/bridge/download",
UpdateFile: "https://protonmail.com/download/bridge_upgrade_linux.tgz",
InstallerFile: "https://protonmail.com/download/Bridge-Installer.sh",
DebFile: "https://protonmail.com/download/protonmail-bridge_1.1.6-1_amd64.deb",
RpmFile: "https://protonmail.com/download/protonmail-bridge-1.1.6-1.x86_64.rpm",
PkgFile: "https://protonmail.com/download/PKGBUILD",
}
version, err := updates.getLatestVersion()
require.NoError(t, err)
require.Equal(t, expectedVersion, version)
}
func TestStartUpgrade(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
if runtime.GOOS != "windows" {
t.Skip("skipping test because only upgrading on windows is currently supported by tests.")
}
updates := newTestUpdates("1")
progress := make(chan Progress, 1)
done := make(chan error)
go func() {
for current := range progress {
log.Infof("progress descr: %d processed %f err %v", current.Description, current.Processed, current.Err)
if current.Err != nil {
done <- current.Err
break
}
}
done <- nil
}()
updates.StartUpgrade(progress)
close(progress)
require.NoError(t, <-done)
}
func newTestUpdates(version string) *Updates {
u := NewBridge(testUpdateDir)
u.version = version
u.revision = "rev123"
u.buildTime = "42"
u.releaseNotes = "• new feature"
u.releaseFixedBugs = "• fixed foo"
return u
}

View File

@ -0,0 +1,49 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package updates
import (
"runtime"
"strings"
)
type VersionInfo struct {
Version string
Revision string
ReleaseDate string // Timestamp generated automatically
ReleaseNotes string // List of features, new line separated with leading dot e.g. `• example\n`
ReleaseFixedBugs string // List of fixed bugs, same usage as release notes
FixedBugs []string // Deprecated list of fixed bugs keeping for backward compatibility (mandatory for working versions up to 1.1.5)
URL string // Open browser and download (obsolete replaced by InstallerFile)
LandingPage string // landing page for manual download
UpdateFile string // automatic update file
InstallerFile string `json:",omitempty"` // manual update file
DebFile string `json:",omitempty"` // debian package file
RpmFile string `json:",omitempty"` // red hat package file
PkgFile string `json:",omitempty"` // arch PKGBUILD file
}
func (info *VersionInfo) GetDownloadLink() string {
switch runtime.GOOS {
case "linux":
return strings.Join([]string{info.DebFile, info.RpmFile, info.PkgFile}, "\n")
default:
return info.InstallerFile
}
}

View File

@ -34,7 +34,7 @@ const (
sep = "\x00"
itemLengthBridge = 9
itemLengthImportExport = 6 // Old format for Import/Export.
itemLengthImportExport = 6 // Old format for Import-Export.
)
var (

View File

@ -299,12 +299,11 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassphra
return errors.Wrap(err, "failed to update API user")
}
emails := []string{}
for _, address := range client.Addresses() {
if u.useOnlyActiveAddresses && address.Receive != pmapi.CanReceive {
continue
}
emails = append(emails, address.Email)
var emails []string //nolint[prealloc]
if u.useOnlyActiveAddresses {
emails = client.Addresses().ActiveEmails()
} else {
emails = client.Addresses().AllEmails()
}
if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), hashedPassphrase, emails); err != nil {