diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 17bebce5..00ba87a8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -77,7 +77,6 @@ dependency-updates:
build-linux:
stage: build
- # Test build every time (= we want to know build is possible).
only:
- branches
script:
@@ -88,6 +87,18 @@ build-linux:
- bridge_*.tgz
expire_in: 2 week
+build-ie-linux:
+ stage: build
+ only:
+ - branches
+ script:
+ - make build-ie
+ artifacts:
+ name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
+ paths:
+ - bridge_*.tgz
+ expire_in: 2 week
+
build-darwin:
stage: build
only:
@@ -114,6 +125,31 @@ build-darwin:
- bridge_*.tgz
expire_in: 2 week
+build-ie-darwin:
+ stage: build
+ only:
+ - branches
+ before_script:
+ - eval $(ssh-agent -s)
+ - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
+ - export PATH=/usr/local/bin:$PATH
+ - export PATH=/usr/local/opt/git/bin:$PATH
+ - export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
+ - export PATH=/usr/local/opt/go@1.13/bin:$PATH
+ - export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
+ - export GOPATH=~/go
+ - export PATH=$GOPATH/bin:$PATH
+ cache: {}
+ tags:
+ - macOS-bridge
+ script:
+ - make build-ie
+ artifacts:
+ name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
+ paths:
+ - bridge_*.tgz
+ expire_in: 2 week
+
build-windows:
stage: build
services:
@@ -136,6 +172,30 @@ build-windows:
- bridge_*.tgz
expire_in: 2 week
+build-ie-windows:
+ stage: build
+ services:
+ - docker:dind
+ only:
+ - branches
+ variables:
+ DOCKER_HOST: tcp://docker:2375
+ script:
+ # We need to install docker because qtdeploy builds for windows inside a docker container.
+ # Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
+ - curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
+ - apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
+ - ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
+ - go mod download
+ - TARGET_OS=windows make build-ie
+ artifacts:
+ name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
+ paths:
+ - bridge_*.tgz
+ expire_in: 2 week
+
+# Stage: MIRROR
+
mirror-repo:
stage: mirror
only:
diff --git a/Makefile b/Makefile
index 0d959d64..56f40189 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,12 @@ TARGET_OS?=${GOOS}
## Build
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
-APP_VERSION?=$(shell git describe --abbrev=0 --tags)-git
+BRIDGE_APP_VERSION?=1.4.0-git
+IE_APP_VERSION?=1.0.0-git
+APP_VERSION=${BRIDGE_APP_VERSION}
+ifeq "${TARGET_CMD}" "Import-Export"
+ APP_VERSION=${IE_APP_VERSION}
+endif
REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z)
diff --git a/README.md b/README.md
index 059d018c..f512e1d6 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ or
### Integration testing
- `TEST_ENV`: set which env to use (fake or live)
+- `TEST_APP`: set which app to test (bridge or ie)
- `TEST_ACCOUNTS`: set JSON file with configured accounts
- `TAGS`: set build tags for tests
- `FEATURES`: set feature dir, file or scenario to test
diff --git a/cmd/Desktop-Bridge/main.go b/cmd/Desktop-Bridge/main.go
index 1c48218f..0dc459b3 100644
--- a/cmd/Desktop-Bridge/main.go
+++ b/cmd/Desktop-Bridge/main.go
@@ -35,18 +35,13 @@ package main
*/
import (
- "fmt"
"io/ioutil"
"os"
- "os/exec"
- "path/filepath"
- "runtime"
"runtime/pprof"
- "strconv"
- "strings"
"github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/internal/cmd"
"github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
@@ -54,104 +49,42 @@ import (
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
- "github.com/ProtonMail/proton-bridge/pkg/args"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/allan-simon/go-singleinstance"
- "github.com/getsentry/raven-go"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
-// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
-// Different number will drop old files and create new ones.
-const cacheVersion = "c11"
+const (
+ // cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
+ // Different number will drop old files and create new ones.
+ cacheVersion = "c11"
+
+ appName = "bridge"
+)
var (
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
-
- // How many crashes in a row.
- numberOfCrashes = 0 //nolint[gochecknoglobals]
-
- // After how many crashes bridge gives up starting.
- maxAllowedCrashes = 10 //nolint[gochecknoglobals]
)
func main() {
- if err := raven.SetDSN(constants.DSNSentry); err != nil {
- log.WithError(err).Errorln("Can not setup sentry DSN")
- }
- raven.SetRelease(constants.Revision)
-
- args.FilterProcessSerialNumberFromArgs()
- filterRestartNumberFromArgs()
-
- app := cli.NewApp()
- app.Name = "Protonmail Bridge"
- app.Version = constants.BuildVersion
- app.Flags = []cli.Flag{
- cli.StringFlag{
- Name: "log-level, l",
- Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
- cli.BoolFlag{
- Name: "no-window",
- Usage: "Don't show window after start"},
- cli.BoolFlag{
- Name: "cli, c",
- Usage: "Use command line interface"},
- cli.BoolFlag{
- Name: "noninteractive",
- Usage: "Start Bridge entirely noninteractively"},
- cli.StringFlag{
- Name: "version-json, g",
- Usage: "Generate json version file"},
- cli.BoolFlag{
- Name: "mem-prof, m",
- Usage: "Generate memory profile"},
- cli.BoolFlag{
- Name: "cpu-prof, p",
- Usage: "Generate CPU profile"},
- }
- app.Usage = "ProtonMail IMAP and SMTP Bridge"
- app.Action = run
-
- // Always log the basic info about current bridge.
- logrus.SetLevel(logrus.InfoLevel)
- log.WithField("version", constants.Version).
- WithField("revision", constants.Revision).
- WithField("runtime", runtime.GOOS).
- WithField("build", constants.BuildTime).
- WithField("args", os.Args).
- WithField("appLong", app.Name).
- WithField("appShort", constants.AppShortName).
- Info("Run app")
- if err := app.Run(os.Args); err != nil {
- log.Error("Program exited with error: ", err)
- }
-}
-
-type panicHandler struct {
- cfg *config.Config
- err *error // Pointer to error of cli action.
-}
-
-func (ph *panicHandler) HandlePanic() {
- r := recover()
- if r == nil {
- return
- }
-
- config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r))
- frontend.HandlePanic("ProtonMail Bridge")
-
- *ph.err = cli.NewExitError("Panic and restart", 255)
- numberOfCrashes++
- log.Error("Restarting after panic")
- restartApp()
- os.Exit(255)
+ cmd.Main(
+ "ProtonMail Bridge",
+ "ProtonMail IMAP and SMTP Bridge",
+ []cli.Flag{
+ cli.BoolFlag{
+ Name: "no-window",
+ Usage: "Don't show window after start"},
+ cli.BoolFlag{
+ Name: "noninteractive",
+ Usage: "Start Bridge entirely noninteractively"},
+ },
+ run,
+ )
}
// run initializes and starts everything in a precise order.
@@ -159,13 +92,17 @@ func (ph *panicHandler) HandlePanic() {
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
- cfg := config.New(constants.AppShortName, constants.Version, constants.Revision, cacheVersion)
+ cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
// We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash
// report which will not be possible if no folder can be created. That's the
// only problem we will not be notified about in any way.
- panicHandler := &panicHandler{cfg, &contextError}
+ panicHandler := &cmd.PanicHandler{
+ AppName: "ProtonMail Bridge",
+ Config: cfg,
+ Err: &contextError,
+ }
defer panicHandler.HandlePanic()
// First we need config and create necessary folder; it's dependency for everything.
@@ -177,13 +114,6 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
logLevel := context.GlobalString("log-level")
debugClient, debugServer := config.SetupLog(cfg, logLevel)
- // Should be called after logs are configured but before preferences are created.
- migratePreferencesFromC10(cfg)
-
- if err := cfg.ClearOldData(); err != nil {
- log.Error("Cannot clear old data: ", err)
- }
-
// Doesn't make sense to continue when Bridge was invoked with wrong arguments.
// We should tell that to the user before we do anything else.
if context.Args().First() != "" {
@@ -193,21 +123,16 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// It's safe to get version JSON file even when other instance is running.
// (thus we put it before check of presence of other Bridge instance).
- updates := updates.New(
- constants.AppShortName,
- constants.Version,
- constants.Revision,
- constants.BuildTime,
- bridge.ReleaseNotes,
- bridge.ReleaseFixedBugs,
- cfg.GetUpdateDir(),
- )
+ updates := updates.NewBridge(cfg.GetUpdateDir())
if dir := context.GlobalString("version-json"); dir != "" {
- generateVersionFiles(updates, dir)
+ cmd.GenerateVersionFiles(updates, dir)
return nil
}
+ // Should be called after logs are configured but before preferences are created.
+ migratePreferencesFromC10(cfg)
+
// ClearOldData before starting new bridge to do a proper setup.
//
// IMPORTANT: If you the change position of this you will need to wait
@@ -234,7 +159,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
if err != nil {
log.Warn("Bridge is already running")
if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil {
- numberOfCrashes = maxAllowedCrashes
+ cmd.DisableRestart()
log.Error("Second instance: ", err)
}
return cli.NewExitError("Bridge is already running.", 3)
@@ -254,7 +179,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
}
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
- defer makeMemoryProfile()
+ defer cmd.MakeMemoryProfile()
}
// Now we initialize all Bridge parts.
@@ -262,7 +187,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
eventListener := listener.New()
events.SetupEvents(eventListener)
- credentialsStore, credentialsError := credentials.NewStore("bridge")
+ credentialsStore, credentialsError := credentials.NewStore(appName)
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
@@ -338,7 +263,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
}
if frontend.IsAppRestarting() {
- restartApp()
+ cmd.RestartApp()
}
return nil
@@ -348,7 +273,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// It will happen only when c10/prefs.json exists and c11/prefs.json not.
// No configuration changed between c10 and c11 versions.
func migratePreferencesFromC10(cfg *config.Config) {
- pref10Path := config.New(constants.AppShortName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
+ pref10Path := config.New(appName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
return
@@ -374,68 +299,3 @@ func migratePreferencesFromC10(cfg *config.Config) {
log.Info("Preferences migrated")
}
-
-// generateVersionFiles writes a JSON file with details about current build.
-// Those files are used for upgrading the app.
-func generateVersionFiles(updates *updates.Updates, dir string) {
- log.Info("Generating version files")
- for _, goos := range []string{"windows", "darwin", "linux"} {
- log.Debug("Generating JSON for ", goos)
- if err := updates.CreateJSONAndSign(dir, goos); err != nil {
- log.Error(err)
- }
- }
-}
-
-func makeMemoryProfile() {
- name := "./mem.pprof"
- f, err := os.Create(name)
- if err != nil {
- log.Error("Could not create memory profile: ", err)
- }
- if abs, err := filepath.Abs(name); err == nil {
- name = abs
- }
- 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)
- }
- _ = f.Close()
-}
-
-// filterRestartNumberFromArgs removes flag with a number how many restart we already did.
-// See restartApp how that number is used.
-func filterRestartNumberFromArgs() {
- tmp := os.Args[:0]
- for i, arg := range os.Args {
- if !strings.HasPrefix(arg, "--restart_") {
- tmp = append(tmp, arg)
- continue
- }
- var err error
- numberOfCrashes, err = strconv.Atoi(os.Args[i][10:])
- if err != nil {
- numberOfCrashes = maxAllowedCrashes
- }
- }
- os.Args = tmp
-}
-
-// restartApp starts a new instance in background.
-func restartApp() {
- if numberOfCrashes >= maxAllowedCrashes {
- log.Error("Too many crashes")
- return
- }
- if exeFile, err := os.Executable(); err == nil {
- arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes))
- cmd := exec.Command(exeFile, arguments...) //nolint[gosec]
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Stdin = os.Stdin
- if err := cmd.Start(); err != nil {
- log.Error("Restart failed: ", err)
- }
- }
-}
diff --git a/cmd/Import-Export/main.go b/cmd/Import-Export/main.go
index f605afc7..ac653ca3 100644
--- a/cmd/Import-Export/main.go
+++ b/cmd/Import-Export/main.go
@@ -18,108 +18,40 @@
package main
import (
- "fmt"
"os"
- "os/exec"
- "path/filepath"
- "runtime"
"runtime/pprof"
- "strconv"
- "strings"
+ "github.com/ProtonMail/proton-bridge/internal/cmd"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
- "github.com/ProtonMail/proton-bridge/pkg/args"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/updates"
- "github.com/getsentry/raven-go"
+ "github.com/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
+const (
+ appName = "importExport"
+ appNameDash = "import-export"
+)
+
var (
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
-
- // How many crashes in a row.
- numberOfCrashes = 0 //nolint[gochecknoglobals]
-
- // After how many crashes import/export gives up starting.
- maxAllowedCrashes = 10 //nolint[gochecknoglobals]
)
func main() {
- constants.AppShortName = "importExport" //TODO
-
- if err := raven.SetDSN(constants.DSNSentry); err != nil {
- log.WithError(err).Errorln("Can not setup sentry DSN")
- }
- raven.SetRelease(constants.Revision)
-
- args.FilterProcessSerialNumberFromArgs()
- filterRestartNumberFromArgs()
-
- app := cli.NewApp()
- app.Name = "Protonmail Import/Export"
- app.Version = constants.BuildVersion
- app.Flags = []cli.Flag{
- cli.StringFlag{
- Name: "log-level, l",
- Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
- cli.BoolFlag{
- Name: "cli, c",
- Usage: "Use command line interface"},
- cli.StringFlag{
- Name: "version-json, g",
- Usage: "Generate json version file"},
- cli.BoolFlag{
- Name: "mem-prof, m",
- Usage: "Generate memory profile"},
- cli.BoolFlag{
- Name: "cpu-prof, p",
- Usage: "Generate CPU profile"},
- }
- app.Usage = "ProtonMail Import/Export"
- app.Action = run
-
- // Always log the basic info about current import/export.
- logrus.SetLevel(logrus.InfoLevel)
- log.WithField("version", constants.Version).
- WithField("revision", constants.Revision).
- WithField("runtime", runtime.GOOS).
- WithField("build", constants.BuildTime).
- WithField("args", os.Args).
- WithField("appLong", app.Name).
- WithField("appShort", constants.AppShortName).
- Info("Run app")
- if err := app.Run(os.Args); err != nil {
- log.Error("Program exited with error: ", err)
- }
-}
-
-type panicHandler struct {
- cfg *config.Config
- err *error // Pointer to error of cli action.
-}
-
-func (ph *panicHandler) HandlePanic() {
- r := recover()
- if r == nil {
- return
- }
-
- config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r))
- frontend.HandlePanic("ProtonMail Import-Export")
-
- *ph.err = cli.NewExitError("Panic and restart", 255)
- numberOfCrashes++
- log.Error("Restarting after panic")
- restartApp()
- os.Exit(255)
+ cmd.Main(
+ "ProtonMail Import/Export",
+ "ProtonMail Import/Export tool",
+ nil,
+ run,
+ )
}
// run initializes and starts everything in a precise order.
@@ -127,13 +59,17 @@ func (ph *panicHandler) HandlePanic() {
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
- cfg := config.New(constants.AppShortName, constants.Version, constants.Revision, "")
+ cfg := config.New(appName, constants.Version, constants.Revision, "")
// We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash
// report which will not be possible if no folder can be created. That's the
// only problem we will not be notified about in any way.
- panicHandler := &panicHandler{cfg, &contextError}
+ panicHandler := &cmd.PanicHandler{
+ AppName: "ProtonMail Import/Export",
+ Config: cfg,
+ Err: &contextError,
+ }
defer panicHandler.HandlePanic()
// First we need config and create necessary folder; it's dependency for everything.
@@ -154,21 +90,22 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// It's safe to get version JSON file even when other instance is running.
// (thus we put it before check of presence of other Import/Export instance).
- updates := updates.New(
- constants.AppShortName,
- constants.Version,
- constants.Revision,
- constants.BuildTime,
- importexport.ReleaseNotes,
- importexport.ReleaseFixedBugs,
- cfg.GetUpdateDir(),
- )
+ updates := updates.NewImportExport(cfg.GetUpdateDir())
if dir := context.GlobalString("version-json"); dir != "" {
- generateVersionFiles(updates, dir)
+ cmd.GenerateVersionFiles(updates, dir)
return nil
}
+ // Now we can try to proceed with starting the import/export. First we need to ensure
+ // this is the only instance. If not, we will end and focus the existing one.
+ lock, err := singleinstance.CreateLockFile(cfg.GetLockPath())
+ if err != nil {
+ log.Warn("Import/Export is already running")
+ return cli.NewExitError("Import/Export is already running.", 3)
+ }
+ defer lock.Close() //nolint[errcheck]
+
// In case user wants to do CPU or memory profiles...
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
f, err := os.Create("cpu.pprof")
@@ -182,7 +119,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
}
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
- defer makeMemoryProfile()
+ defer cmd.MakeMemoryProfile()
}
// Now we initialize all Import/Export parts.
@@ -190,7 +127,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
eventListener := listener.New()
events.SetupEvents(eventListener)
- credentialsStore, credentialsError := credentials.NewStore("import-export")
+ credentialsStore, credentialsError := credentials.NewStore(appNameDash)
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
@@ -224,73 +161,8 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
}
if frontend.IsAppRestarting() {
- restartApp()
+ cmd.RestartApp()
}
return nil
}
-
-// generateVersionFiles writes a JSON file with details about current build.
-// Those files are used for upgrading the app.
-func generateVersionFiles(updates *updates.Updates, dir string) {
- log.Info("Generating version files")
- for _, goos := range []string{"windows", "darwin", "linux"} {
- log.Debug("Generating JSON for ", goos)
- if err := updates.CreateJSONAndSign(dir, goos); err != nil {
- log.Error(err)
- }
- }
-}
-
-func makeMemoryProfile() {
- name := "./mem.pprof"
- f, err := os.Create(name)
- if err != nil {
- log.Error("Could not create memory profile: ", err)
- }
- if abs, err := filepath.Abs(name); err == nil {
- name = abs
- }
- 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)
- }
- _ = f.Close()
-}
-
-// filterRestartNumberFromArgs removes flag with a number how many restart we already did.
-// See restartApp how that number is used.
-func filterRestartNumberFromArgs() {
- tmp := os.Args[:0]
- for i, arg := range os.Args {
- if !strings.HasPrefix(arg, "--restart_") {
- tmp = append(tmp, arg)
- continue
- }
- var err error
- numberOfCrashes, err = strconv.Atoi(os.Args[i][10:])
- if err != nil {
- numberOfCrashes = maxAllowedCrashes
- }
- }
- os.Args = tmp
-}
-
-// restartApp starts a new instance in background.
-func restartApp() {
- if numberOfCrashes >= maxAllowedCrashes {
- log.Error("Too many crashes")
- return
- }
- if exeFile, err := os.Executable(); err == nil {
- arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes))
- cmd := exec.Command(exeFile, arguments...) //nolint[gosec]
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Stdin = os.Stdin
- if err := cmd.Start(); err != nil {
- log.Error("Restart failed: ", err)
- }
- }
-}
diff --git a/go.mod b/go.mod
index cb4c80ba..c499d754 100644
--- a/go.mod
+++ b/go.mod
@@ -35,6 +35,7 @@ require (
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
github.com/emersion/go-mbox v1.0.0
+ github.com/emersion/go-message v0.11.1
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
diff --git a/pkg/args/args.go b/internal/cmd/args.go
similarity index 90%
rename from pkg/args/args.go
rename to internal/cmd/args.go
index 12edbb20..79532e0d 100644
--- a/pkg/args/args.go
+++ b/internal/cmd/args.go
@@ -15,16 +15,16 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-package args
+package cmd
import (
"os"
"strings"
)
-// FilterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber
+// filterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber
// http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951
-func FilterProcessSerialNumberFromArgs() {
+func filterProcessSerialNumberFromArgs() {
tmp := os.Args[:0]
for _, arg := range os.Args {
if !strings.Contains(arg, "-psn_") {
diff --git a/internal/cmd/main.go b/internal/cmd/main.go
new file mode 100644
index 00000000..e83d806b
--- /dev/null
+++ b/internal/cmd/main.go
@@ -0,0 +1,86 @@
+// 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 .
+
+package cmd
+
+import (
+ "os"
+ "runtime"
+
+ "github.com/ProtonMail/proton-bridge/pkg/constants"
+ "github.com/getsentry/raven-go"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var (
+ log = logrus.WithField("pkg", "cmd") //nolint[gochecknoglobals]
+
+ baseFlags = []cli.Flag{ //nolint[gochecknoglobals]
+ cli.StringFlag{
+ Name: "log-level, l",
+ Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
+ cli.BoolFlag{
+ Name: "cli, c",
+ Usage: "Use command line interface"},
+ cli.StringFlag{
+ Name: "version-json, g",
+ Usage: "Generate json version file"},
+ cli.BoolFlag{
+ Name: "mem-prof, m",
+ Usage: "Generate memory profile"},
+ cli.BoolFlag{
+ Name: "cpu-prof, p",
+ Usage: "Generate CPU profile"},
+ }
+)
+
+// Main sets up Sentry, filters out unwanted args, creates app and runs it.
+func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) {
+ if err := raven.SetDSN(constants.DSNSentry); err != nil {
+ log.WithError(err).Errorln("Can not setup sentry DSN")
+ }
+ raven.SetRelease(constants.Revision)
+
+ filterProcessSerialNumberFromArgs()
+ filterRestartNumberFromArgs()
+
+ app := newApp(appName, usage, extraFlags, run)
+
+ logrus.SetLevel(logrus.InfoLevel)
+ log.WithField("version", constants.Version).
+ WithField("revision", constants.Revision).
+ WithField("build", constants.BuildTime).
+ WithField("runtime", runtime.GOOS).
+ WithField("args", os.Args).
+ WithField("appName", app.Name).
+ Info("Run app")
+
+ if err := app.Run(os.Args); err != nil {
+ log.Error("Program exited with error: ", err)
+ }
+}
+
+func newApp(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) *cli.App {
+ app := cli.NewApp()
+ app.Name = appName
+ app.Usage = usage
+ app.Version = constants.BuildVersion
+ app.Flags = append(baseFlags, extraFlags...) //nolint[gocritic]
+ app.Action = run
+ return app
+}
diff --git a/internal/cmd/memory_profile.go b/internal/cmd/memory_profile.go
new file mode 100644
index 00000000..5b09bf54
--- /dev/null
+++ b/internal/cmd/memory_profile.go
@@ -0,0 +1,43 @@
+// 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 .
+
+package cmd
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "runtime/pprof"
+)
+
+// 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)
+ }
+ if abs, err := filepath.Abs(name); err == nil {
+ name = abs
+ }
+ 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)
+ }
+ _ = f.Close()
+}
diff --git a/internal/cmd/restart.go b/internal/cmd/restart.go
new file mode 100644
index 00000000..abd3036e
--- /dev/null
+++ b/internal/cmd/restart.go
@@ -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 .
+
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+
+ "github.com/ProtonMail/proton-bridge/internal/frontend"
+ "github.com/ProtonMail/proton-bridge/pkg/config"
+ "github.com/urfave/cli"
+)
+
+const (
+ // After how many crashes app gives up starting.
+ maxAllowedCrashes = 10
+)
+
+var (
+ // How many crashes happened so far in a row.
+ // It will be filled from args by `filterRestartNumberFromArgs`.
+ // Every call of `HandlePanic` will increase this number.
+ // Then it will be passed as argument to the next try by `RestartApp`.
+ numberOfCrashes = 0 //nolint[gochecknoglobals]
+)
+
+// filterRestartNumberFromArgs removes flag with a number how many restart we already did.
+// See restartApp how that number is used.
+func filterRestartNumberFromArgs() {
+ tmp := os.Args[:0]
+ for i, arg := range os.Args {
+ if !strings.HasPrefix(arg, "--restart_") {
+ tmp = append(tmp, arg)
+ continue
+ }
+ var err error
+ numberOfCrashes, err = strconv.Atoi(os.Args[i][10:])
+ if err != nil {
+ numberOfCrashes = maxAllowedCrashes
+ }
+ }
+ os.Args = tmp
+}
+
+// DisableRestart disables restart once `RestartApp` is called.
+func DisableRestart() {
+ numberOfCrashes = maxAllowedCrashes
+}
+
+// RestartApp starts a new instance in background.
+func RestartApp() {
+ if numberOfCrashes >= maxAllowedCrashes {
+ log.Error("Too many crashes")
+ return
+ }
+ if exeFile, err := os.Executable(); err == nil {
+ arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes))
+ cmd := exec.Command(exeFile, arguments...) //nolint[gosec]
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Stdin = os.Stdin
+ if err := cmd.Start(); err != nil {
+ log.Error("Restart failed: ", err)
+ }
+ }
+}
+
+// PanicHandler defines HandlePanic which can be used anywhere in defer.
+type PanicHandler struct {
+ AppName string
+ Config *config.Config
+ Err *error // Pointer to error of cli action.
+}
+
+// HandlePanic should be called in defer to ensure restart of app after error.
+func (ph *PanicHandler) HandlePanic() {
+ r := recover()
+ if r == nil {
+ return
+ }
+
+ config.HandlePanic(ph.Config, fmt.Sprintf("Recover: %v", r))
+ frontend.HandlePanic(ph.AppName)
+
+ *ph.Err = cli.NewExitError("Panic and restart", 255)
+ numberOfCrashes++
+ log.Error("Restarting after panic")
+ RestartApp()
+ os.Exit(255)
+}
diff --git a/internal/cmd/version_file.go b/internal/cmd/version_file.go
new file mode 100644
index 00000000..fe0e8e9c
--- /dev/null
+++ b/internal/cmd/version_file.go
@@ -0,0 +1,32 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package cmd
+
+import "github.com/ProtonMail/proton-bridge/pkg/updates"
+
+// GenerateVersionFiles writes a JSON file with details about current build.
+// Those files are used for upgrading the app.
+func GenerateVersionFiles(updates *updates.Updates, dir string) {
+ log.Info("Generating version files")
+ for _, goos := range []string{"windows", "darwin", "linux"} {
+ log.Debug("Generating JSON for ", goos)
+ if err := updates.CreateJSONAndSign(dir, goos); err != nil {
+ log.Error(err)
+ }
+ }
+}
diff --git a/internal/frontend/cli-ie/frontend.go b/internal/frontend/cli-ie/frontend.go
index 7bb11586..cf769fe6 100644
--- a/internal/frontend/cli-ie/frontend.go
+++ b/internal/frontend/cli-ie/frontend.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-// Package cli provides CLI interface of the Bridge.
+// Package cli provides CLI interface of the Import/Export.
package cli
import (
@@ -227,7 +227,11 @@ func (f *frontendCLI) Loop(credentialsError error) error {
return credentialsError
}
- f.Print(`Welcome to ProtonMail Import/Export interactive shell`)
+ f.Print(`
+Welcome to ProtonMail Import/Export interactive shell
+
+WARNING: CLI is experimental feature and does not cover all functionality yet.
+ `)
f.Run()
return nil
}
diff --git a/internal/frontend/cli-ie/updates.go b/internal/frontend/cli-ie/updates.go
index b30da468..62332dc8 100644
--- a/internal/frontend/cli-ie/updates.go
+++ b/internal/frontend/cli-ie/updates.go
@@ -20,7 +20,7 @@ package cli
import (
"strings"
- "github.com/ProtonMail/proton-bridge/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/pkg/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 Bridge "+versionInfo.Version), "\n")
+ f.Println(bold("ProtonMail Import/Export "+versionInfo.Version), "\n")
if versionInfo.ReleaseNotes != "" {
f.Println(bold("Release Notes"))
f.Println(versionInfo.ReleaseNotes)
@@ -59,7 +59,7 @@ func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
}
func (f *frontendCLI) printCredits(c *ishell.Context) {
- for _, pkg := range strings.Split(bridge.Credits, ";") {
+ for _, pkg := range strings.Split(importexport.Credits, ";") {
f.Println(pkg)
}
}
diff --git a/internal/frontend/cli-ie/utils.go b/internal/frontend/cli-ie/utils.go
index f5a97000..6f01d2fe 100644
--- a/internal/frontend/cli-ie/utils.go
+++ b/internal/frontend/cli-ie/utils.go
@@ -98,7 +98,7 @@ func (f *frontendCLI) notifyNeedUpgrade() {
func (f *frontendCLI) notifyCredentialsError() {
// Print in 80-column width.
- f.Println("ProtonMail Bridge 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 Bridge 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.
diff --git a/internal/frontend/cli/updates.go b/internal/frontend/cli/updates.go
index 55d35e16..b30da468 100644
--- a/internal/frontend/cli/updates.go
+++ b/internal/frontend/cli/updates.go
@@ -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 Bridge "+versionInfo.Version), "\n")
if versionInfo.ReleaseNotes != "" {
f.Println(bold("Release Notes"))
f.Println(versionInfo.ReleaseNotes)
diff --git a/internal/frontend/qt-common/path_status.go b/internal/frontend/qt-common/path_status.go
index e6845f92..bf76d6d8 100644
--- a/internal/frontend/qt-common/path_status.go
+++ b/internal/frontend/qt-common/path_status.go
@@ -67,12 +67,12 @@ func CheckPathStatus(path string) int {
tmpFile += "tmp"
_, err = os.Lstat(tmpFile)
}
- err = os.Mkdir(tmpFile, 0777)
+ err = os.Mkdir(tmpFile, 0750)
if err != nil {
stat |= PathWrongPermissions
return int(stat)
}
- os.Remove(tmpFile)
+ _ = os.Remove(tmpFile)
} else {
stat |= PathNotADir
}
diff --git a/internal/importexport/credits.go b/internal/importexport/credits.go
index 87eed1d7..311da61d 100644
--- a/internal/importexport/credits.go
+++ b/internal/importexport/credits.go
@@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-// Code generated by ./credits.sh at Thu 04 Jun 2020 04:19:16 PM CEST. DO NOT EDIT.
+// Code generated by ./credits.sh at Mon Jul 13 14:02:21 CEST 2020. DO NOT EDIT.
package importexport
-const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/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-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/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/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;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;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-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
diff --git a/internal/importexport/release_notes.go b/internal/importexport/release_notes.go
index 456f472c..71713e72 100644
--- a/internal/importexport/release_notes.go
+++ b/internal/importexport/release_notes.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see .
-// Code generated by ./release-notes.sh at 'Thu Jun 4 15:54:31 CEST 2020'. DO NOT EDIT.
+// Code generated by ./release-notes.sh at 'Thu Jun 25 10:06:16 CEST 2020'. DO NOT EDIT.
package importexport
diff --git a/internal/transfer/mailbox.go b/internal/transfer/mailbox.go
index b4fbaf46..430db7c9 100644
--- a/internal/transfer/mailbox.go
+++ b/internal/transfer/mailbox.go
@@ -41,13 +41,9 @@ func (m Mailbox) Hash() string {
// LeastUsedColor is intended to return color for creating a new inbox or label
func LeastUsedColor(mailboxes []Mailbox) string {
usedColors := []string{}
-
- if mailboxes != nil {
- for _, m := range mailboxes {
- usedColors = append(usedColors, m.Color)
- }
+ for _, m := range mailboxes {
+ usedColors = append(usedColors, m.Color)
}
-
return pmapi.LeastUsedColor(usedColors)
}
diff --git a/internal/transfer/message.go b/internal/transfer/message.go
index 5c939305..fec7be38 100644
--- a/internal/transfer/message.go
+++ b/internal/transfer/message.go
@@ -56,6 +56,10 @@ type MessageStatus struct {
Time time.Time
}
+func (status *MessageStatus) String() string {
+ return fmt.Sprintf("%s (%s, %s, %s): %s", status.SourceID, status.Subject, status.From, status.Time, status.GetErrorMessage())
+}
+
func (status *MessageStatus) setDetailsFromHeader(header mail.Header) {
dec := &mime.WordDecoder{}
diff --git a/internal/transfer/progress.go b/internal/transfer/progress.go
index ee463f45..28c37f38 100644
--- a/internal/transfer/progress.go
+++ b/internal/transfer/progress.go
@@ -139,8 +139,10 @@ func (p *Progress) messageExported(messageID string, body []byte, err error) {
p.log.WithField("id", messageID).WithError(err).Debug("Message exported")
status := p.messageStatuses[messageID]
- status.exported = true
status.exportErr = err
+ if err == nil {
+ status.exported = true
+ }
if len(body) > 0 {
status.bodyHash = fmt.Sprintf("%x", sha256.Sum256(body))
@@ -166,8 +168,10 @@ func (p *Progress) messageImported(messageID, importID string, err error) {
p.log.WithField("id", messageID).WithError(err).Debug("Message imported")
p.messageStatuses[messageID].targetID = importID
- p.messageStatuses[messageID].imported = true
p.messageStatuses[messageID].importErr = err
+ if err == nil {
+ p.messageStatuses[messageID].imported = true
+ }
// Import is the last step, now we can log the result to the report file.
p.logMessage(messageID)
diff --git a/internal/transfer/progress_test.go b/internal/transfer/progress_test.go
index 68baeb16..3ec170d2 100644
--- a/internal/transfer/progress_test.go
+++ b/internal/transfer/progress_test.go
@@ -73,8 +73,8 @@ func TestProgressAddingMessages(t *testing.T) {
failed, imported, exported, added, _ := progress.GetCounts()
a.Equal(t, uint(4), added)
- a.Equal(t, uint(4), exported)
- a.Equal(t, uint(3), imported)
+ a.Equal(t, uint(2), exported)
+ a.Equal(t, uint(2), imported)
a.Equal(t, uint(3), failed)
errorsMap := map[string]string{}
diff --git a/internal/transfer/provider_imap_source.go b/internal/transfer/provider_imap_source.go
index 59dbf9ac..82d13f1c 100644
--- a/internal/transfer/provider_imap_source.go
+++ b/internal/transfer/provider_imap_source.go
@@ -68,7 +68,7 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres
continue
}
- messagesInfo := p.loadMessagesInfo(rule, progress, mailbox.UidValidity)
+ messagesInfo := p.loadMessagesInfo(rule, progress, mailbox.UidValidity, mailbox.Messages)
res[rule.SourceMailbox.Name] = messagesInfo
progress.updateCount(rule.SourceMailbox.Name, uint(len(messagesInfo)))
}
@@ -76,7 +76,7 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres
return res
}
-func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValidity uint32) map[string]imapMessageInfo {
+func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValidity, count uint32) map[string]imapMessageInfo {
messagesInfo := map[string]imapMessageInfo{}
pageStart := uint32(1)
@@ -86,6 +86,11 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
break
}
+ // Some servers do not accept message sequence number higher than the total count.
+ if pageEnd > count {
+ pageEnd = count
+ }
+
seqSet := &imap.SeqSet{}
seqSet.AddRange(pageStart, pageEnd)
@@ -114,7 +119,7 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
}
progress.callWrap(func() error {
- return p.fetch(seqSet, items, processMessageCallback)
+ return p.fetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
})
if pageMsgCount < imapPageSize {
@@ -145,7 +150,6 @@ func (p *IMAPProvider) transferTo(rule *Rule, messagesInfo map[string]imapMessag
if seqSetSize != 0 && (seqSetSize+messageInfo.size) > imapMaxFetchSize {
log.WithField("mailbox", rule.SourceMailbox.Name).WithField("seq", seqSet).WithField("size", seqSetSize).Debug("Fetching messages")
-
p.exportMessages(rule, progress, ch, seqSet, uidToID)
seqSet = &imap.SeqSet{}
@@ -157,6 +161,11 @@ func (p *IMAPProvider) transferTo(rule *Rule, messagesInfo map[string]imapMessag
seqSetSize += messageInfo.size
uidToID[messageInfo.uid] = messageInfo.id
}
+
+ if len(uidToID) != 0 {
+ log.WithField("mailbox", rule.SourceMailbox.Name).WithField("seq", seqSet).WithField("size", seqSetSize).Debug("Fetching messages")
+ p.exportMessages(rule, progress, ch, seqSet, uidToID)
+ }
}
func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<- Message, seqSet *imap.SeqSet, uidToID map[uint32]string) {
@@ -188,7 +197,7 @@ func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<-
}
progress.callWrap(func() error {
- return p.uidFetch(seqSet, items, processMessageCallback)
+ return p.uidFetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
})
}
diff --git a/internal/transfer/provider_imap_utils.go b/internal/transfer/provider_imap_utils.go
index 364e9ecc..36b3cb2c 100644
--- a/internal/transfer/provider_imap_utils.go
+++ b/internal/transfer/provider_imap_utils.go
@@ -49,16 +49,11 @@ func (l *imapErrorLogger) Println(v ...interface{}) {
l.log.Errorln(v...)
}
-type imapDebugLogger struct { //nolint[unused]
- log *logrus.Entry
-}
-
-func (l *imapDebugLogger) Write(data []byte) (int, error) {
- l.log.Trace(string(data))
- return len(data), nil
-}
-
func (p *IMAPProvider) ensureConnection(callback func() error) error {
+ return p.ensureConnectionAndSelection(callback, "")
+}
+
+func (p *IMAPProvider) ensureConnectionAndSelection(callback func() error, ensureSelectedIn string) error {
var callErr error
for i := 1; i <= imapRetries; i++ {
callErr = callback()
@@ -66,8 +61,8 @@ func (p *IMAPProvider) ensureConnection(callback func() error) error {
return nil
}
- log.WithField("attempt", i).WithError(callErr).Warning("Call failed, trying reconnect")
- err := p.tryReconnect()
+ log.WithField("attempt", i).WithError(callErr).Warning("IMAP call failed, trying reconnect")
+ err := p.tryReconnect(ensureSelectedIn)
if err != nil {
return err
}
@@ -75,7 +70,7 @@ func (p *IMAPProvider) ensureConnection(callback func() error) error {
return errors.Wrap(callErr, "too many retries")
}
-func (p *IMAPProvider) tryReconnect() error {
+func (p *IMAPProvider) tryReconnect(ensureSelectedIn string) error {
start := time.Now()
var previousErr error
for {
@@ -84,6 +79,7 @@ func (p *IMAPProvider) tryReconnect() error {
}
err := pmapi.CheckConnection()
+ log.WithError(err).Debug("Connection check")
if err != nil {
time.Sleep(imapReconnectSleep)
previousErr = err
@@ -91,24 +87,47 @@ func (p *IMAPProvider) tryReconnect() error {
}
err = p.reauth()
+ log.WithError(err).Debug("Reauth")
if err != nil {
time.Sleep(imapReconnectSleep)
previousErr = err
continue
}
+ if ensureSelectedIn != "" {
+ _, err = p.client.Select(ensureSelectedIn, true)
+ log.WithError(err).Debug("Reselect")
+ if err != nil {
+ previousErr = err
+ continue
+ }
+ }
+
break
}
return nil
}
func (p *IMAPProvider) reauth() error {
- if _, err := p.client.Capability(); err != nil {
- state := p.client.State()
- log.WithField("addr", p.addr).WithField("state", state).WithError(err).Debug("Reconnecting")
- p.client = nil
+ var state imap.ConnState
+
+ // In some cases once go-imap fails, we cannot issue another command
+ // because it would dead-lock. Let's simply ignore it, we want to open
+ // new connection anyway.
+ ch := make(chan struct{})
+ go func() {
+ defer close(ch)
+ if _, err := p.client.Capability(); err != nil {
+ state = p.client.State()
+ }
+ }()
+ select {
+ case <-ch:
+ case <-time.After(30 * time.Second):
}
+ log.WithField("addr", p.addr).WithField("state", state).Debug("Reconnecting")
+ p.client = nil
return p.auth()
}
@@ -121,15 +140,25 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
return errors.Wrap(err, "failed to dial server")
}
- client, err := imapClient.DialTLS(p.addr, nil)
+ var client *imapClient.Client
+ var err error
+ host, _, _ := net.SplitHostPort(p.addr)
+ if host == "127.0.0.1" {
+ client, err = imapClient.Dial(p.addr)
+ } else {
+ client, err = imapClient.DialTLS(p.addr, nil)
+ }
if err != nil {
return errors.Wrap(err, "failed to connect to server")
}
+
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
- // Logrus have Writer helper but it fails for big messages because of
- // bufio.MaxScanTokenSize limit.
- // This spams a lot, uncomment once needed during development.
- //client.SetDebug(&imapDebugLogger{logrus.WithField("pkg", "imap-client")})
+ // Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
+ // Also, this spams a lot, uncomment once needed during development.
+ //client.SetDebug(imap.NewDebugWriter(
+ // logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
+ // logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
+ //))
p.client = client
log.Info("Connected")
@@ -205,16 +234,16 @@ func (p *IMAPProvider) selectIn(mailboxName string) (mailbox *imap.MailboxStatus
return
}
-func (p *IMAPProvider) fetch(seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error {
- return p.fetchHelper(false, seqSet, items, processMessageCallback)
+func (p *IMAPProvider) fetch(ensureSelectedIn string, seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error {
+ return p.fetchHelper(false, ensureSelectedIn, seqSet, items, processMessageCallback)
}
-func (p *IMAPProvider) uidFetch(seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error {
- return p.fetchHelper(true, seqSet, items, processMessageCallback)
+func (p *IMAPProvider) uidFetch(ensureSelectedIn string, seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error {
+ return p.fetchHelper(true, ensureSelectedIn, seqSet, items, processMessageCallback)
}
-func (p *IMAPProvider) fetchHelper(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error {
- return p.ensureConnection(func() error {
+func (p *IMAPProvider) fetchHelper(uid bool, ensureSelectedIn string, seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error {
+ return p.ensureConnectionAndSelection(func() error {
messagesCh := make(chan *imap.Message)
doneCh := make(chan error)
@@ -232,5 +261,5 @@ func (p *IMAPProvider) fetchHelper(uid bool, seqSet *imap.SeqSet, items []imap.F
err := <-doneCh
return err
- })
+ }, ensureSelectedIn)
}
diff --git a/internal/transfer/provider_mbox_target.go b/internal/transfer/provider_mbox_target.go
index 610f8cf2..44f450f3 100644
--- a/internal/transfer/provider_mbox_target.go
+++ b/internal/transfer/provider_mbox_target.go
@@ -45,7 +45,7 @@ func (p *MBOXProvider) TransferFrom(rules transferRules, progress *Progress, ch
defer log.Info("Finished transfer from channel to MBOX")
for msg := range ch {
- for progress.shouldStop() {
+ if progress.shouldStop() {
break
}
diff --git a/internal/transfer/provider_pmapi.go b/internal/transfer/provider_pmapi.go
index f6f14e27..61f708de 100644
--- a/internal/transfer/provider_pmapi.go
+++ b/internal/transfer/provider_pmapi.go
@@ -20,7 +20,7 @@ package transfer
import (
"sort"
- "github.com/ProtonMail/gopenpgp/crypto"
+ "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
)
@@ -31,6 +31,9 @@ type PMAPIProvider struct {
userID string
addressID string
keyRing *crypto.KeyRing
+
+ importMsgReqMap map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
+ importMsgReqSize int
}
// NewPMAPIProvider returns new PMAPIProvider.
@@ -45,6 +48,9 @@ func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*P
userID: userID,
addressID: addressID,
keyRing: keyRing,
+
+ importMsgReqMap: map[string]*pmapi.ImportMsgReq{},
+ importMsgReqSize: 0,
}, nil
}
diff --git a/internal/transfer/provider_pmapi_source.go b/internal/transfer/provider_pmapi_source.go
index 240a4a84..6c383812 100644
--- a/internal/transfer/provider_pmapi_source.go
+++ b/internal/transfer/provider_pmapi_source.go
@@ -19,6 +19,7 @@ package transfer
import (
"fmt"
+ "sync"
pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@@ -32,11 +33,21 @@ func (p *PMAPIProvider) TransferTo(rules transferRules, progress *Progress, ch c
log.Info("Started transfer from PMAPI to channel")
defer log.Info("Finished transfer from PMAPI to channel")
- go p.loadCounts(rules, progress)
+ // TransferTo cannot end sooner than loadCounts goroutine because
+ // loadCounts writes to channel in progress which would be closed.
+ // That can happen for really small accounts.
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ p.loadCounts(rules, progress)
+ }()
for rule := range rules.iterateActiveRules() {
p.transferTo(rule, progress, ch, rules.skipEncryptedMessages)
}
+
+ wg.Wait()
}
func (p *PMAPIProvider) loadCounts(rules transferRules, progress *Progress) {
diff --git a/internal/transfer/provider_pmapi_target.go b/internal/transfer/provider_pmapi_target.go
index 1034caa4..7e146825 100644
--- a/internal/transfer/provider_pmapi_target.go
+++ b/internal/transfer/provider_pmapi_target.go
@@ -28,6 +28,11 @@ import (
"github.com/pkg/errors"
)
+const (
+ pmapiImportBatchMaxItems = 10
+ pmapiImportBatchMaxSize = 25 * 1000 * 1000 // 25 MB
+)
+
// DefaultMailboxes returns the default mailboxes for default rules if no other is found.
func (p *PMAPIProvider) DefaultMailboxes(_ Mailbox) []Mailbox {
return []Mailbox{{
@@ -67,18 +72,19 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
defer log.Info("Finished transfer from channel to PMAPI")
for msg := range ch {
- for progress.shouldStop() {
+ if progress.shouldStop() {
break
}
- var importedID string
- var err error
if p.isMessageDraft(msg) {
- importedID, err = p.importDraft(msg, rules.globalMailbox)
+ p.transferDraft(rules, progress, msg)
} else {
- importedID, err = p.importMessage(msg, rules.globalMailbox)
+ p.transferMessage(rules, progress, msg)
}
- progress.messageImported(msg.ID, importedID, err)
+ }
+
+ if len(p.importMsgReqMap) > 0 {
+ p.importMessages(progress)
}
}
@@ -91,6 +97,11 @@ func (p *PMAPIProvider) isMessageDraft(msg Message) bool {
return false
}
+func (p *PMAPIProvider) transferDraft(rules transferRules, progress *Progress, msg Message) {
+ importedID, err := p.importDraft(msg, rules.globalMailbox)
+ progress.messageImported(msg.ID, importedID, err)
+}
+
func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string, error) {
message, attachmentReaders, err := p.parseMessage(msg)
if err != nil {
@@ -138,15 +149,30 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
return draft.ID, nil
}
-func (p *PMAPIProvider) importMessage(msg Message, globalMailbox *Mailbox) (string, error) {
+func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress, msg Message) {
+ importMsgReq, err := p.generateImportMsgReq(msg, rules.globalMailbox)
+ if err != nil {
+ progress.messageImported(msg.ID, "", err)
+ return
+ }
+
+ importMsgReqSize := len(importMsgReq.Body)
+ if p.importMsgReqSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.importMsgReqMap) == pmapiImportBatchMaxItems {
+ p.importMessages(progress)
+ }
+ p.importMsgReqMap[msg.ID] = importMsgReq
+ p.importMsgReqSize += importMsgReqSize
+}
+
+func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox) (*pmapi.ImportMsgReq, error) {
message, attachmentReaders, err := p.parseMessage(msg)
if err != nil {
- return "", errors.Wrap(err, "failed to parse message")
+ return nil, errors.Wrap(err, "failed to parse message")
}
body, err := p.encryptMessage(message, attachmentReaders)
if err != nil {
- return "", errors.Wrap(err, "failed to encrypt message")
+ return nil, errors.Wrap(err, "failed to encrypt message")
}
unread := 0
@@ -165,26 +191,14 @@ func (p *PMAPIProvider) importMessage(msg Message, globalMailbox *Mailbox) (stri
labelIDs = append(labelIDs, globalMailbox.ID)
}
- importMsgReq := &pmapi.ImportMsgReq{
+ return &pmapi.ImportMsgReq{
AddressID: p.addressID,
Body: body,
Unread: unread,
Time: message.Time,
Flags: computeMessageFlags(labelIDs),
LabelIDs: labelIDs,
- }
-
- results, err := p.importRequest([]*pmapi.ImportMsgReq{importMsgReq})
- if err != nil {
- return "", errors.Wrap(err, "failed to import messages")
- }
- if len(results) == 0 {
- return "", errors.New("import ended with no result")
- }
- if results[0].Error != nil {
- return "", errors.Wrap(results[0].Error, "failed to import message")
- }
- return results[0].MessageID, nil
+ }, nil
}
func (p *PMAPIProvider) parseMessage(msg Message) (*pmapi.Message, []io.Reader, error) {
@@ -218,3 +232,65 @@ func computeMessageFlags(labels []string) (flag int64) {
return flag
}
+
+func (p *PMAPIProvider) importMessages(progress *Progress) {
+ if progress.shouldStop() {
+ return
+ }
+
+ importMsgIDs := []string{}
+ importMsgRequests := []*pmapi.ImportMsgReq{}
+ for msgID, req := range p.importMsgReqMap {
+ importMsgIDs = append(importMsgIDs, msgID)
+ importMsgRequests = append(importMsgRequests, req)
+ }
+
+ log.WithField("msgIDs", importMsgIDs).WithField("size", p.importMsgReqSize).Debug("Importing messages")
+ results, err := p.importRequest(importMsgRequests)
+
+ // In case the whole request failed, try to import every message one by one.
+ if err != nil || len(results) == 0 {
+ log.WithError(err).Warning("Importing messages failed, trying one by one")
+ for msgID, req := range p.importMsgReqMap {
+ importedID, err := p.importMessage(progress, req)
+ progress.messageImported(msgID, importedID, err)
+ }
+ return
+ }
+
+ // In case request passed but some messages failed, try to import the failed ones alone.
+ for index, result := range results {
+ msgID := importMsgIDs[index]
+ if result.Error != nil {
+ log.WithError(result.Error).WithField("msg", msgID).Warning("Importing message failed, trying alone")
+ req := importMsgRequests[index]
+ importedID, err := p.importMessage(progress, req)
+ progress.messageImported(msgID, importedID, err)
+ } else {
+ progress.messageImported(msgID, result.MessageID, nil)
+ }
+ }
+
+ p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{}
+ p.importMsgReqSize = 0
+}
+
+func (p *PMAPIProvider) importMessage(progress *Progress, req *pmapi.ImportMsgReq) (importedID string, importedErr error) {
+ progress.callWrap(func() error {
+ results, err := p.importRequest([]*pmapi.ImportMsgReq{req})
+ if err != nil {
+ return errors.Wrap(err, "failed to import messages")
+ }
+ if len(results) == 0 {
+ importedErr = errors.New("import ended with no result")
+ return nil // This should not happen, only when there is bug which means we should skip this one.
+ }
+ if results[0].Error != nil {
+ importedErr = errors.Wrap(results[0].Error, "failed to import message")
+ return nil // Call passed but API refused this message, skip this one.
+ }
+ importedID = results[0].MessageID
+ return nil
+ })
+ return
+}
diff --git a/internal/transfer/provider_pmapi_test.go b/internal/transfer/provider_pmapi_test.go
index 6d529fae..76ce8327 100644
--- a/internal/transfer/provider_pmapi_test.go
+++ b/internal/transfer/provider_pmapi_test.go
@@ -178,17 +178,16 @@ func setupPMAPIClientExpectationForExport(m *mocks) {
func setupPMAPIClientExpectationForImport(m *mocks) {
m.pmapiClient.EXPECT().KeyRingForAddressID(gomock.Any()).Return(m.keyring, nil).AnyTimes()
m.pmapiClient.EXPECT().Import(gomock.Any()).DoAndReturn(func(requests []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
- r.Equal(m.t, 1, len(requests))
-
- request := requests[0]
- for _, msgID := range []string{"msg1", "msg2"} {
- if bytes.Contains(request.Body, []byte(msgID)) {
- return []*pmapi.ImportMsgRes{{MessageID: msgID, Error: nil}}, nil
+ results := []*pmapi.ImportMsgRes{}
+ for _, request := range requests {
+ for _, msgID := range []string{"msg1", "msg2"} {
+ if bytes.Contains(request.Body, []byte(msgID)) {
+ results = append(results, &pmapi.ImportMsgRes{MessageID: msgID, Error: nil})
+ }
}
}
- r.Fail(m.t, "No message found")
- return nil, nil
- }).Times(2)
+ return results, nil
+ }).AnyTimes()
}
func setupPMAPIClientExpectationForImportDraft(m *mocks) {
diff --git a/internal/transfer/provider_pmapi_utils.go b/internal/transfer/provider_pmapi_utils.go
index 65bf8817..0633e52e 100644
--- a/internal/transfer/provider_pmapi_utils.go
+++ b/internal/transfer/provider_pmapi_utils.go
@@ -39,7 +39,7 @@ func (p *PMAPIProvider) ensureConnection(callback func() error) error {
return nil
}
- log.WithField("attempt", i).WithError(callErr).Warning("Call failed, trying reconnect")
+ log.WithField("attempt", i).WithError(callErr).Warning("API call failed, trying reconnect")
err := p.tryReconnect()
if err != nil {
return err
@@ -57,6 +57,7 @@ func (p *PMAPIProvider) tryReconnect() error {
}
err := p.clientManager.CheckConnection()
+ log.WithError(err).Debug("Connection check")
if err != nil {
time.Sleep(pmapiReconnectSleep)
previousErr = err
diff --git a/internal/transfer/transfer_test.go b/internal/transfer/transfer_test.go
index 9a7c0bf7..2d0128f9 100644
--- a/internal/transfer/transfer_test.go
+++ b/internal/transfer/transfer_test.go
@@ -18,11 +18,10 @@
package transfer
import (
- "bytes"
"io/ioutil"
"testing"
- "github.com/ProtonMail/gopenpgp/crypto"
+ "github.com/ProtonMail/gopenpgp/v2/crypto"
transfermocks "github.com/ProtonMail/proton-bridge/internal/transfer/mocks"
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
gomock "github.com/golang/mock/gomock"
@@ -62,11 +61,12 @@ func newTestKeyring() *crypto.KeyRing {
if err != nil {
panic(err)
}
- userKey, err := crypto.ReadArmoredKeyRing(bytes.NewReader(data))
+ key, err := crypto.NewKeyFromArmored(string(data))
if err != nil {
panic(err)
}
- if err := userKey.Unlock([]byte("testpassphrase")); err != nil {
+ userKey, err := crypto.NewKeyRing(key)
+ if err != nil {
panic(err)
}
return userKey
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index 02a46e8b..19c617b4 100644
--- a/pkg/constants/constants.go
+++ b/pkg/constants/constants.go
@@ -29,9 +29,6 @@ var (
// BuildTime stamp of the build.
BuildTime = ""
- // AppShortName to make setup.
- AppShortName = "bridge"
-
// DSNSentry client keys to be able to report crashes to Sentry.
DSNSentry = ""
diff --git a/pkg/message/build.go b/pkg/message/build.go
index f25cd09b..290da24e 100644
--- a/pkg/message/build.go
+++ b/pkg/message/build.go
@@ -27,7 +27,7 @@ import (
"mime/quotedprintable"
"net/textproto"
- "github.com/ProtonMail/gopenpgp/crypto"
+ "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-textwrapper"
openpgperrors "golang.org/x/crypto/openpgp/errors"
diff --git a/pkg/pmapi/clientmanager.go b/pkg/pmapi/clientmanager.go
index b351347f..a2a91e88 100644
--- a/pkg/pmapi/clientmanager.go
+++ b/pkg/pmapi/clientmanager.go
@@ -317,9 +317,8 @@ func (cm *ClientManager) CheckConnection() error {
retStatus := make(chan error)
retAPI := make(chan error)
- // Check protonstatus.com without SSL for performance reasons. vpn_status endpoint is fast and
- // returns only OK; this endpoint is not known by the public. We check the connection only.
- go checkConnection(client, "http://protonstatus.com/vpn_status", retStatus)
+ // vpn_status endpoint is fast and returns only OK. We check the connection only.
+ go checkConnection(client, "https://protonstatus.com/vpn_status", retStatus)
// Check of API reachability also uses a fast endpoint.
go checkConnection(client, cm.GetRootURL()+"/tests/ping", retAPI)
@@ -351,7 +350,7 @@ func (cm *ClientManager) CheckConnection() error {
func CheckConnection() error {
client := &http.Client{Timeout: time.Second * 10}
retStatus := make(chan error)
- checkConnection(client, "http://protonstatus.com/vpn_status", retStatus)
+ go checkConnection(client, "https://protonstatus.com/vpn_status", retStatus)
return <-retStatus
}
diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go
index 2b0a9607..07463d49 100644
--- a/pkg/updates/updates.go
+++ b/pkg/updates/updates.go
@@ -27,6 +27,9 @@ import (
"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"
)
@@ -57,7 +60,6 @@ var (
)
type Updates struct {
- appName string
version string
revision string
buildTime string
@@ -68,26 +70,44 @@ type Updates struct {
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).
- macAppBundleName string // For update procedure.
+ 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.
}
-// New inits Updates struct.
-// `appName` should be in camelCase format for file names. For installer files is converted to CamelCase.
-func New(appName, version, revision, buildTime, releaseNotes, releaseFixedBugs, updateTempDir string) *Updates {
+// NewBridge inits Updates struct for bridge.
+func NewBridge(updateTempDir string) *Updates {
return &Updates{
- appName: appName,
- version: version,
- revision: revision,
- buildTime: buildTime,
- releaseNotes: releaseNotes,
- releaseFixedBugs: releaseFixedBugs,
+ version: constants.Version,
+ revision: constants.Revision,
+ buildTime: constants.BuildTime,
+ releaseNotes: bridge.ReleaseNotes,
+ releaseFixedBugs: bridge.ReleaseFixedBugs,
updateTempDir: updateTempDir,
- landingPagePath: appName + "/download",
- installerFileBaseName: strings.Title(appName) + "-Installer",
+ landingPagePath: "bridge/download",
+ installerFileBaseName: "Bridge-Installer",
versionFileBaseName: "current_version",
- updateFileBaseName: appName + "_upgrade",
- macAppBundleName: "ProtonMail " + strings.Title(appName) + ".app", // For update procedure.
+ 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: "blog/import-export-beta/",
+ installerFileBaseName: "Import-Export-Installer",
+ versionFileBaseName: "current_version_ie",
+ updateFileBaseName: "ie_upgrade",
+ linuxFileBaseName: "protonmail-import-export",
+ macAppBundleName: "ProtonMail Import-Export.app",
}
}
@@ -165,7 +185,7 @@ func (u *Updates) getLocalVersion(goos string) VersionInfo {
}
if goos == "linux" {
- pkgName := "protonmail-" + u.appName
+ pkgName := u.linuxFileBaseName
pkgRel := "1"
pkgBase := strings.Join([]string{Host, DownloadPath, pkgName}, "/")
diff --git a/pkg/updates/updates_test.go b/pkg/updates/updates_test.go
index 220864d1..c0e89b40 100644
--- a/pkg/updates/updates_test.go
+++ b/pkg/updates/updates_test.go
@@ -174,5 +174,11 @@ func TestStartUpgrade(t *testing.T) {
}
func newTestUpdates(version string) *Updates {
- return New("bridge", version, "rev123", "42", "• new feature", "• fixed foo", testUpdateDir)
+ u := NewBridge(testUpdateDir)
+ u.version = version
+ u.revision = "rev123"
+ u.buildTime = "42"
+ u.releaseNotes = "• new feature"
+ u.releaseFixedBugs = "• fixed foo"
+ return u
}
diff --git a/test/Makefile b/test/Makefile
index be965951..0a512f97 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -1,25 +1,38 @@
-.PHONY: check-has-go install-godog test test-live test-debug test-live-debug
+.PHONY: check-go check-godog install-godog test test-bridge test-ie test-live test-live-bridge test-live-ie test-stage test-debug test-live-debug bench
export GO111MODULE=on
export BRIDGE_VERSION:=1.3.0-integrationtests
export VERBOSITY?=fatal
export TEST_DATA=testdata
+export TEST_APP?=bridge
-check-has-go:
+# Tests do not run in parallel. This will overrule user settings.
+MAKEFLAGS=-j1
+
+check-go:
@which go || (echo "Install Go-lang!" && exit 1)
-
-install-godog: check-has-go
+check-godog:
+ @which godog || $(MAKE) install-godog
+install-godog: check-go
go get github.com/cucumber/godog/cmd/godog@v0.8.1
-test:
- which godog || $(MAKE) install-godog
- TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json godog --tags="~@ignore" $(FEATURES)
+test: test-bridge test-ie
+test-bridge: FEATURES ?= features/bridge
+test-bridge: check-godog
+ TEST_APP=bridge TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json godog --tags="~@ignore" $(FEATURES)
+test-ie: FEATURES ?= features/ie
+test-ie: check-godog
+ TEST_APP=ie TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json godog --tags="~@ignore" $(FEATURES)
# Doesn't work in parallel!
# Provide TEST_ACCOUNTS with your accounts.
-test-live:
- which godog || $(MAKE) install-godog
- TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES)
+test-live: test-live-bridge test-live-ie
+test-live-bridge: FEATURES ?= features/bridge
+test-live-bridge: check-godog
+ TEST_APP=bridge TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES)
+test-live-ie: FEATURES ?= features/ie
+test-live-ie: check-godog
+ TEST_APP=ie TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES)
# Doesn't work in parallel!
# Provide TEST_ACCOUNTS with your accounts.
diff --git a/test/api_actions_test.go b/test/api_actions_test.go
new file mode 100644
index 00000000..5bab3ddf
--- /dev/null
+++ b/test/api_actions_test.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package tests
+
+import (
+ "time"
+
+ "github.com/cucumber/godog"
+)
+
+func APIActionsFeatureContext(s *godog.Suite) {
+ s.Step(`^the internet connection is lost$`, theInternetConnectionIsLost)
+ s.Step(`^the internet connection is restored$`, theInternetConnectionIsRestored)
+ s.Step(`^(\d+) seconds pass$`, secondsPass)
+}
+
+func theInternetConnectionIsLost() error {
+ ctx.GetPMAPIController().TurnInternetConnectionOff()
+ return nil
+}
+
+func theInternetConnectionIsRestored() error {
+ ctx.GetPMAPIController().TurnInternetConnectionOn()
+ return nil
+}
+
+func secondsPass(seconds int) error {
+ time.Sleep(time.Duration(seconds) * time.Second)
+ return nil
+}
diff --git a/test/api_checks_test.go b/test/api_checks_test.go
index 089f7767..b2f6d456 100644
--- a/test/api_checks_test.go
+++ b/test/api_checks_test.go
@@ -29,6 +29,8 @@ import (
func APIChecksFeatureContext(s *godog.Suite) {
s.Step(`^API endpoint "([^"]*)" is called with:$`, apiIsCalledWith)
s.Step(`^message is sent with API call:$`, messageIsSentWithAPICall)
+ s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages)
+ s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages)
}
func apiIsCalledWith(endpoint string, data *gherkin.DocString) error {
@@ -77,3 +79,41 @@ func checkAllRequiredFieldsForSendingMessage(request []byte) bool {
}
return true
}
+
+func apiMailboxForUserHasMessages(mailboxName, bddUserID string, messages *gherkin.DataTable) error {
+ return apiMailboxForAddressOfUserHasMessages(mailboxName, "", bddUserID, messages)
+}
+
+func apiMailboxForAddressOfUserHasMessages(mailboxName, bddAddressID, bddUserID string, messages *gherkin.DataTable) error {
+ account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
+ if account == nil {
+ return godog.ErrPending
+ }
+
+ labelIDs, err := ctx.GetPMAPIController().GetLabelIDs(account.Username(), []string{mailboxName})
+ if err != nil {
+ return internalError(err, "getting label %s for %s", mailboxName, account.Username())
+ }
+ labelID := labelIDs[0]
+
+ pmapiMessages, err := ctx.GetPMAPIController().GetMessages(account.Username(), labelID)
+ if err != nil {
+ return err
+ }
+
+ head := messages.Rows[0].Cells
+ for _, row := range messages.Rows[1:] {
+ found, err := messagesContainsMessageRow(account, pmapiMessages, head, row)
+ if err != nil {
+ return err
+ }
+ if !found {
+ rowMap := map[string]string{}
+ for idx, cell := range row.Cells {
+ rowMap[head[idx].Value] = cell.Value
+ }
+ return fmt.Errorf("message %v not found", rowMap)
+ }
+ }
+ return nil
+}
diff --git a/test/api_setup_test.go b/test/api_setup_test.go
new file mode 100644
index 00000000..71232f87
--- /dev/null
+++ b/test/api_setup_test.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package tests
+
+import (
+ "github.com/cucumber/godog"
+)
+
+func APISetupFeatureContext(s *godog.Suite) {
+ s.Step(`^there is no internet connection$`, thereIsNoInternetConnection)
+}
+
+func thereIsNoInternetConnection() error {
+ ctx.GetPMAPIController().TurnInternetConnectionOff()
+ return nil
+}
diff --git a/test/bdd_test.go b/test/bdd_test.go
index 17104b51..0b072183 100644
--- a/test/bdd_test.go
+++ b/test/bdd_test.go
@@ -18,19 +18,27 @@
package tests
import (
+ "os"
+
"github.com/ProtonMail/proton-bridge/test/context"
"github.com/cucumber/godog"
)
+const (
+ timeFormat = "2006-01-02T15:04:05"
+)
+
func FeatureContext(s *godog.Suite) {
s.BeforeScenario(beforeScenario)
s.AfterScenario(afterScenario)
+ APIActionsFeatureContext(s)
APIChecksFeatureContext(s)
+ APISetupFeatureContext(s)
BridgeActionsFeatureContext(s)
- BridgeChecksFeatureContext(s)
- BridgeSetupFeatureContext(s)
+
+ CommonChecksFeatureContext(s)
IMAPActionsAuthFeatureContext(s)
IMAPActionsMailboxFeatureContext(s)
@@ -45,18 +53,32 @@ func FeatureContext(s *godog.Suite) {
StoreActionsFeatureContext(s)
StoreChecksFeatureContext(s)
StoreSetupFeatureContext(s)
+
+ TransferActionsFeatureContext(s)
+ TransferChecksFeatureContext(s)
+ TransferSetupFeatureContext(s)
+
+ UsersActionsFeatureContext(s)
+ UsersSetupFeatureContext(s)
+ UsersChecksFeatureContext(s)
}
var ctx *context.TestContext //nolint[gochecknoglobals]
func beforeScenario(scenario interface{}) {
- ctx = context.New()
+ // bridge or ie. With godog 0.10.x and later it can be determined from
+ // scenario.Uri and its file location.
+ app := os.Getenv("TEST_APP")
+ ctx = context.New(app)
}
func afterScenario(scenario interface{}, err error) {
if err != nil {
- for _, user := range ctx.GetBridge().GetUsers() {
- user.GetStore().TestDumpDB(ctx.GetTestingT())
+ for _, user := range ctx.GetUsers().GetUsers() {
+ store := user.GetStore()
+ if store != nil {
+ store.TestDumpDB(ctx.GetTestingT())
+ }
}
}
ctx.Cleanup()
diff --git a/test/benchmarks/bench_test.go b/test/benchmarks/bench_test.go
index c157dc3b..6e6486ea 100644
--- a/test/benchmarks/bench_test.go
+++ b/test/benchmarks/bench_test.go
@@ -27,7 +27,7 @@ import (
)
func benchTestContext() (*context.TestContext, *mocks.IMAPClient) {
- ctx := context.New()
+ ctx := context.New("bridge")
username := "user"
account := ctx.GetTestAccount(username)
diff --git a/test/bridge_actions_test.go b/test/bridge_actions_test.go
index 0c2bb449..5b93ca4d 100644
--- a/test/bridge_actions_test.go
+++ b/test/bridge_actions_test.go
@@ -18,28 +18,16 @@
package tests
import (
- "time"
-
"github.com/cucumber/godog"
)
func BridgeActionsFeatureContext(s *godog.Suite) {
s.Step(`^bridge starts$`, bridgeStarts)
s.Step(`^bridge syncs "([^"]*)"$`, bridgeSyncsUser)
- s.Step(`^"([^"]*)" logs in to bridge$`, userLogsInToBridge)
- s.Step(`^"([^"]*)" logs in to bridge with bad password$`, userLogsInToBridgeWithBadPassword)
- s.Step(`^"([^"]*)" logs out from bridge$`, userLogsOutFromBridge)
- s.Step(`^"([^"]*)" changes the address mode$`, userChangesTheAddressMode)
- s.Step(`^user deletes "([^"]*)" from bridge$`, userDeletesUserFromBridge)
- s.Step(`^user deletes "([^"]*)" from bridge with cache$`, userDeletesUserFromBridgeWithCache)
- s.Step(`^the internet connection is lost$`, theInternetConnectionIsLost)
- s.Step(`^the internet connection is restored$`, theInternetConnectionIsRestored)
- s.Step(`^(\d+) seconds pass$`, secondsPass)
- s.Step(`^"([^"]*)" swaps address "([^"]*)" with address "([^"]*)"$`, swapsAddressWithAddress)
}
func bridgeStarts() error {
- ctx.SetLastBridgeError(ctx.RestartBridge())
+ ctx.SetLastError(ctx.RestartBridge())
return nil
}
@@ -51,113 +39,6 @@ func bridgeSyncsUser(bddUserID string) error {
if err := ctx.WaitForSync(account.Username()); err != nil {
return internalError(err, "waiting for sync")
}
- ctx.SetLastBridgeError(ctx.GetTestingError())
+ ctx.SetLastError(ctx.GetTestingError())
return nil
}
-
-func userLogsInToBridge(bddUserID string) error {
- account := ctx.GetTestAccount(bddUserID)
- if account == nil {
- return godog.ErrPending
- }
- ctx.SetLastBridgeError(ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()))
- return nil
-}
-
-func userLogsInToBridgeWithBadPassword(bddUserID string) error {
- account := ctx.GetTestAccount(bddUserID)
- if account == nil {
- return godog.ErrPending
- }
- ctx.SetLastBridgeError(ctx.LoginUser(account.Username(), "you shall not pass!", "123"))
- return nil
-}
-
-func userLogsOutFromBridge(bddUserID string) error {
- account := ctx.GetTestAccount(bddUserID)
- if account == nil {
- return godog.ErrPending
- }
- ctx.SetLastBridgeError(ctx.LogoutUser(account.Username()))
- return nil
-}
-
-func userChangesTheAddressMode(bddUserID string) error {
- account := ctx.GetTestAccount(bddUserID)
- if account == nil {
- return godog.ErrPending
- }
- bridgeUser, err := ctx.GetUser(account.Username())
- if err != nil {
- return internalError(err, "getting user %s", account.Username())
- }
- if err := bridgeUser.SwitchAddressMode(); err != nil {
- return err
- }
-
- ctx.EventuallySyncIsFinishedForUsername(account.Username())
- return nil
-}
-
-func userDeletesUserFromBridge(bddUserID string) error {
- return deleteUserFromBridge(bddUserID, false)
-}
-
-func userDeletesUserFromBridgeWithCache(bddUserID string) error {
- return deleteUserFromBridge(bddUserID, true)
-}
-
-func deleteUserFromBridge(bddUserID string, cache bool) error {
- account := ctx.GetTestAccount(bddUserID)
- if account == nil {
- return godog.ErrPending
- }
- bridgeUser, err := ctx.GetUser(account.Username())
- if err != nil {
- return internalError(err, "getting user %s", account.Username())
- }
- return ctx.GetBridge().DeleteUser(bridgeUser.ID(), cache)
-}
-
-func theInternetConnectionIsLost() error {
- ctx.GetPMAPIController().TurnInternetConnectionOff()
- return nil
-}
-
-func theInternetConnectionIsRestored() error {
- ctx.GetPMAPIController().TurnInternetConnectionOn()
- return nil
-}
-
-func secondsPass(seconds int) error {
- time.Sleep(time.Duration(seconds) * time.Second)
- return nil
-}
-
-func swapsAddressWithAddress(bddUserID, bddAddressID1, bddAddressID2 string) error {
- account := ctx.GetTestAccount(bddUserID)
- if account == nil {
- return godog.ErrPending
- }
-
- address1ID := account.GetAddressID(bddAddressID1)
- address2ID := account.GetAddressID(bddAddressID2)
- addressIDs := make([]string, len(*account.Addresses()))
-
- var address1Index, address2Index int
- for i, v := range *account.Addresses() {
- if v.ID == address1ID {
- address1Index = i
- }
- if v.ID == address2ID {
- address2Index = i
- }
- addressIDs[i] = v.ID
- }
-
- addressIDs[address1Index], addressIDs[address2Index] = addressIDs[address2Index], addressIDs[address1Index]
-
- ctx.ReorderAddresses(account.Username(), bddAddressID1, bddAddressID2)
-
- return ctx.GetPMAPIController().ReorderAddresses(account.User(), addressIDs)
-}
diff --git a/test/common_checks_test.go b/test/common_checks_test.go
new file mode 100644
index 00000000..bf1690e5
--- /dev/null
+++ b/test/common_checks_test.go
@@ -0,0 +1,37 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package tests
+
+import (
+ "github.com/cucumber/godog"
+ a "github.com/stretchr/testify/assert"
+)
+
+func CommonChecksFeatureContext(s *godog.Suite) {
+ s.Step(`^last response is "([^"]*)"$`, lastResponseIs)
+}
+
+func lastResponseIs(expectedResponse string) error {
+ err := ctx.GetLastError()
+ if expectedResponse == "OK" {
+ a.NoError(ctx.GetTestingT(), err)
+ } else {
+ a.EqualError(ctx.GetTestingT(), err, expectedResponse)
+ }
+ return ctx.GetTestingError()
+}
diff --git a/test/context/bridge.go b/test/context/bridge.go
index 2d03de8b..711b5f51 100644
--- a/test/context/bridge.go
+++ b/test/context/bridge.go
@@ -32,9 +32,10 @@ func (ctx *TestContext) GetBridge() *bridge.Bridge {
}
// withBridgeInstance creates a bridge instance for use in the test.
-// Every TestContext has this by default and thus this doesn't need to be exported.
+// TestContext has this by default once called with env variable TEST_APP=bridge.
func (ctx *TestContext) withBridgeInstance() {
ctx.bridge = newBridgeInstance(ctx.t, ctx.cfg, ctx.credStore, ctx.listener, ctx.clientManager)
+ ctx.users = ctx.bridge.Users
ctx.addCleanupChecked(ctx.bridge.ClearData, "Cleaning bridge data")
}
@@ -69,13 +70,3 @@ func newBridgeInstance(
pref := preferences.New(cfg)
return bridge.New(cfg, pref, panicHandler, eventListener, clientManager, credStore)
}
-
-// SetLastBridgeError sets the last error that occurred while executing a bridge action.
-func (ctx *TestContext) SetLastBridgeError(err error) {
- ctx.bridgeLastError = err
-}
-
-// GetLastBridgeError returns the last error that occurred while executing a bridge action.
-func (ctx *TestContext) GetLastBridgeError() error {
- return ctx.bridgeLastError
-}
diff --git a/test/context/config.go b/test/context/config.go
index 71dfe89e..1b2e646e 100644
--- a/test/context/config.go
+++ b/test/context/config.go
@@ -77,6 +77,9 @@ func (c *fakeConfig) GetLogPrefix() string {
func (c *fakeConfig) GetPreferencesPath() string {
return filepath.Join(c.dir, "prefs.json")
}
+func (c *fakeConfig) GetTransferDir() string {
+ return c.dir
+}
func (c *fakeConfig) GetTLSCertPath() string {
return filepath.Join(c.dir, "cert.pem")
}
diff --git a/test/context/context.go b/test/context/context.go
index e6db35c7..b614625c 100644
--- a/test/context/context.go
+++ b/test/context/context.go
@@ -20,6 +20,8 @@ package context
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/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@@ -46,10 +48,12 @@ type TestContext struct {
pmapiController PMAPIController
clientManager *pmapi.ClientManager
- // Bridge core related variables.
- bridge *bridge.Bridge
- bridgeLastError error
- credStore users.CredentialsStorer
+ // Core related variables.
+ bridge *bridge.Bridge
+ importExport *importexport.ImportExport
+ users *users.Users
+ credStore users.CredentialsStorer
+ lastError error
// IMAP related variables.
imapAddr string
@@ -63,6 +67,12 @@ type TestContext struct {
smtpClients map[string]*mocks.SMTPClient
smtpLastResponses map[string]*mocks.SMTPResponse
+ // Transfer related variables.
+ transferLocalRootForImport string
+ transferLocalRootForExport string
+ transferRemoteIMAPServer *mocks.IMAPServer
+ transferProgress *transfer.Progress
+
// These are the cleanup steps executed when Cleanup() is called.
cleanupSteps []*Cleaner
@@ -71,7 +81,7 @@ type TestContext struct {
}
// New returns a new test TestContext.
-func New() *TestContext {
+func New(app string) *TestContext {
setLogrusVerbosityFromEnv()
cfg := newFakeConfig()
@@ -96,8 +106,15 @@ func New() *TestContext {
// Ensure that the config is cleaned up after the test is over.
ctx.addCleanupChecked(cfg.ClearData, "Cleaning bridge config data")
- // Create bridge instance under test.
- ctx.withBridgeInstance()
+ // Create bridge or import/export instance under test.
+ switch app {
+ case "bridge":
+ ctx.withBridgeInstance()
+ case "ie":
+ ctx.withImportExportInstance()
+ default:
+ panic("unknown app: " + app)
+ }
return ctx
}
@@ -125,3 +142,13 @@ func (ctx *TestContext) GetTestingT() *bddT { //nolint[golint]
func (ctx *TestContext) GetTestingError() error {
return ctx.t.getErrors()
}
+
+// SetLastError sets the last error that occurred while executing an action.
+func (ctx *TestContext) SetLastError(err error) {
+ ctx.lastError = err
+}
+
+// GetLastError returns the last error that occurred while executing an action.
+func (ctx *TestContext) GetLastError() error {
+ return ctx.lastError
+}
diff --git a/test/context/importexport.go b/test/context/importexport.go
new file mode 100644
index 00000000..6b468d52
--- /dev/null
+++ b/test/context/importexport.go
@@ -0,0 +1,48 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package context
+
+import (
+ "github.com/ProtonMail/proton-bridge/internal/importexport"
+ "github.com/ProtonMail/proton-bridge/internal/users"
+ "github.com/ProtonMail/proton-bridge/pkg/listener"
+)
+
+// GetImportExport returns import/export instance.
+func (ctx *TestContext) GetImportExport() *importexport.ImportExport {
+ return ctx.importExport
+}
+
+// withImportExportInstance creates a import/export instance for use in the test.
+// TestContext has this by default once called with env variable TEST_APP=ie.
+func (ctx *TestContext) withImportExportInstance() {
+ ctx.importExport = newImportExportInstance(ctx.t, ctx.cfg, ctx.credStore, ctx.listener, ctx.clientManager)
+ ctx.users = ctx.importExport.Users
+}
+
+// newImportExportInstance creates a new import/export instance configured to use the given config/credstore.
+func newImportExportInstance(
+ t *bddT,
+ cfg importexport.Configer,
+ credStore users.CredentialsStorer,
+ eventListener listener.Listener,
+ clientManager users.ClientManager,
+) *importexport.ImportExport {
+ panicHandler := &panicHandler{t: t}
+ return importexport.New(cfg, panicHandler, eventListener, clientManager, credStore)
+}
diff --git a/test/context/pmapi_controller.go b/test/context/pmapi_controller.go
index 769771fe..ea36af35 100644
--- a/test/context/pmapi_controller.go
+++ b/test/context/pmapi_controller.go
@@ -33,6 +33,7 @@ type PMAPIController interface {
GetLabelIDs(username string, labelNames []string) ([]string, error)
AddUserMessage(username string, message *pmapi.Message) error
GetMessageID(username, messageIndex string) string
+ GetMessages(username, labelID string) ([]*pmapi.Message, error)
ReorderAddresses(user *pmapi.User, addressIDs []string) error
PrintCalls()
WasCalled(method, path string, expectedRequest []byte) bool
diff --git a/test/context/transfer.go b/test/context/transfer.go
new file mode 100644
index 00000000..a2e792ae
--- /dev/null
+++ b/test/context/transfer.go
@@ -0,0 +1,91 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package context
+
+import (
+ "io/ioutil"
+ "math/rand"
+ "os"
+ "strconv"
+
+ "github.com/ProtonMail/proton-bridge/internal/transfer"
+ "github.com/ProtonMail/proton-bridge/test/mocks"
+)
+
+// SetTransferProgress sets transfer progress.
+func (ctx *TestContext) SetTransferProgress(progress *transfer.Progress) {
+ ctx.transferProgress = progress
+}
+
+// GetTransferProgress returns transfer progress.
+func (ctx *TestContext) GetTransferProgress() *transfer.Progress {
+ return ctx.transferProgress
+}
+
+// GetTransferLocalRootForImport creates temporary root for importing
+// if it not exists yet, and returns its path.
+func (ctx *TestContext) GetTransferLocalRootForImport() string {
+ if ctx.transferLocalRootForImport != "" {
+ return ctx.transferLocalRootForImport
+ }
+ root := ctx.createLocalRoot()
+ ctx.transferLocalRootForImport = root
+ return root
+}
+
+// GetTransferLocalRootForExport creates temporary root for exporting
+// if it not exists yet, and returns its path.
+func (ctx *TestContext) GetTransferLocalRootForExport() string {
+ if ctx.transferLocalRootForExport != "" {
+ return ctx.transferLocalRootForExport
+ }
+ root := ctx.createLocalRoot()
+ ctx.transferLocalRootForExport = root
+ return root
+}
+
+func (ctx *TestContext) createLocalRoot() string {
+ root, err := ioutil.TempDir("", "transfer")
+ if err != nil {
+ panic("failed to create temp transfer root: " + err.Error())
+ }
+
+ ctx.addCleanupChecked(func() error {
+ return os.RemoveAll(root)
+ }, "Cleaning transfer data")
+
+ return root
+}
+
+// GetTransferRemoteIMAPServer creates mocked IMAP server if it not created yet, and returns it.
+func (ctx *TestContext) GetTransferRemoteIMAPServer() *mocks.IMAPServer {
+ if ctx.transferRemoteIMAPServer != nil {
+ return ctx.transferRemoteIMAPServer
+ }
+
+ port := 21300 + rand.Intn(100)
+ ctx.transferRemoteIMAPServer = mocks.NewIMAPServer("user", "pass", "127.0.0.1", strconv.Itoa(port))
+
+ ctx.transferRemoteIMAPServer.Start()
+ ctx.addCleanupChecked(func() error {
+ ctx.transferRemoteIMAPServer.Stop()
+ return nil
+ }, "Cleaning transfer IMAP server")
+
+ return ctx.transferRemoteIMAPServer
+}
diff --git a/test/context/bridge_user.go b/test/context/users.go
similarity index 90%
rename from test/context/bridge_user.go
rename to test/context/users.go
index cdfcbd02..934fd213 100644
--- a/test/context/bridge_user.go
+++ b/test/context/users.go
@@ -30,11 +30,16 @@ import (
"github.com/stretchr/testify/assert"
)
+// GetUsers returns users instance.
+func (ctx *TestContext) GetUsers() *users.Users {
+ return ctx.users
+}
+
// LoginUser logs in the user with the given username, password, and mailbox password.
func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (err error) {
srp.RandReader = rand.New(rand.NewSource(42))
- client, auth, err := ctx.bridge.Login(username, password)
+ client, auth, err := ctx.users.Login(username, password)
if err != nil {
return errors.Wrap(err, "failed to login")
}
@@ -45,7 +50,7 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (e
}
}
- user, err := ctx.bridge.FinishLogin(client, auth, mailboxPassword)
+ user, err := ctx.users.FinishLogin(client, auth, mailboxPassword)
if err != nil {
return errors.Wrap(err, "failed to finish login")
}
@@ -57,7 +62,7 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (e
// GetUser retrieves the bridge user matching the given query string.
func (ctx *TestContext) GetUser(username string) (*users.User, error) {
- return ctx.bridge.GetUser(username)
+ return ctx.users.GetUser(username)
}
// GetStore retrieves the store for given username.
@@ -100,6 +105,9 @@ func (ctx *TestContext) WaitForSync(username string) error {
if err != nil {
return err
}
+ if store == nil {
+ return nil
+ }
// First wait for ongoing sync to be done before starting and waiting for new one.
ctx.eventuallySyncIsFinished(store)
store.TestSync()
@@ -121,7 +129,7 @@ func (ctx *TestContext) EventuallySyncIsFinishedForUsername(username string) {
// LogoutUser logs out the given user.
func (ctx *TestContext) LogoutUser(query string) (err error) {
- user, err := ctx.bridge.GetUser(query)
+ user, err := ctx.users.GetUser(query)
if err != nil {
return errors.Wrap(err, "failed to get user")
}
@@ -135,12 +143,12 @@ func (ctx *TestContext) LogoutUser(query string) (err error) {
// DeleteUser deletes the given user.
func (ctx *TestContext) DeleteUser(query string, deleteStore bool) (err error) {
- user, err := ctx.bridge.GetUser(query)
+ user, err := ctx.users.GetUser(query)
if err != nil {
return errors.Wrap(err, "failed to get user")
}
- if err = ctx.bridge.DeleteUser(user.ID(), deleteStore); err != nil {
+ if err = ctx.users.DeleteUser(user.ID(), deleteStore); err != nil {
err = errors.Wrap(err, "failed to delete user")
}
diff --git a/test/fakeapi/controller_control.go b/test/fakeapi/controller_control.go
index fb4ac497..dafaa67c 100644
--- a/test/fakeapi/controller_control.go
+++ b/test/fakeapi/controller_control.go
@@ -159,3 +159,17 @@ func (ctl *Controller) resetUsers() {
func (ctl *Controller) GetMessageID(username, messageIndex string) string {
return messageIndex
}
+
+func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message, error) {
+ messages := []*pmapi.Message{}
+ for _, fakeAPI := range ctl.fakeAPIs {
+ if fakeAPI.username == username {
+ for _, message := range fakeAPI.messages {
+ if labelID == "" || message.HasLabelID(labelID) {
+ messages = append(messages, message)
+ }
+ }
+ }
+ }
+ return messages, nil
+}
diff --git a/test/fakeapi/keyring_userKey b/test/fakeapi/keyring_userKey
new file mode 100644
index 00000000..976d2be2
--- /dev/null
+++ b/test/fakeapi/keyring_userKey
@@ -0,0 +1,62 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: OpenPGP.js v4.4.5
+Comment: testpassphrase
+
+xcLYBFzGzhEBCADBxfqTFMqfQzT77A5tuuhPFwPq8dfC2evs8u1OvTqFbztY
+5FOuSxzduyeDqQ1Fx6dKEOKgcYE8t1Uh4VSS7z6bTdY8j9yrL81kCVB46sE1
+OzStzyx/5l7OdH/pM4F+aKslnLvqlw0UeJr+UNizVtOCEUaNfVjPK3cc1ocx
+v+36K4RnnyfEtjUW9gDZbhgaF02G5ILHmWmbgM7I+77gCd2wI0EdY9s/JZQ+
+VmkMFqoMdY9PyBchoOIPUkkGQi1SaF4IEzMaAUSbnCYkHHY/SbfDTcR46VGq
+cXlkB1rq5xskaUQ9r+giCC/K4pc7bBkI1lQ7ADVuWvdrWnWapK0FO6CfABEB
+AAEAB/0YPhPJ0phA/EWviN+16bmGVOZNaVapjt2zMMybWmrtEQv3OeWgO3nP
+4cohRi/zaCBCphcm+dxbLhftW7AFi/9PVcR09436MB+oTCQFugpUWw+4TmA5
+BidxTpDxf4X2vH3rquQLBufWL6U7JlPeKAGL1xZ2aCq0DIeOk5D+xTjZizV2
+GIyQRVCLWb+LfDmvvcp3Y94X60KXdBAMuS1ZMKcY3Sl8VAXNB4KQsC/kByzf
+6FCB097XZRYV7lvJJQ7+6Wisb3yVi8sEQx2sFm5fAp+0qi3a6zRTEp49r6Hr
+gyWViH5zOOpA7DcNwx1Bwhi7GG0tak6EUnnKUNLfOupglcphBADmpXCgT4nc
+uSBYTiZSVcB/ICCkTxVsHL1WcXtPK2Ikzussx2n9kb0rapvuC0YLipX9lUkQ
+fyeC3jQJeCyN79AkDGkOfWaESueT2hM0Po+RwDgMibKn6yJ1zebz4Lc2J3C9
+oVFcAnql+9KyGsAPn03fyQzDnvhNnJvHJi4Hx8AWoQQA1xLoXeVBjRi0IjjU
+E6Mqaq5RLEog4kXRp86VSSEGHBwyIYnDiM//gjseo/CXuVyHwL7UXitp8s1B
+D1uE3APrhqUS66fD5pkF+z+RcSqiIv7I76NJ24Cdg38L6seGSjOHrq7/dEeG
+K6WqfQUCEjta3yNSg7pXb2wn2WZqKIK+rz8EALZRuMXeql/FtO3Cjb0sv7oT
+9dLP4cn1bskGRJ+Vok9lfCERbfXGccoAk3V+qSfpHgKxsebkRbUhf+trOGnw
+tW+kBWo/5hYGQuN+A9JogSJViT+nuZyE+x1/rKswDFmlMSdf2GIDARWIV0gc
+b1yOEwUmNBSthPcnFXvBr4BG3XTtNPTNLSJhcm9uMjEtM0BzYWRlbWJlLm9y
+ZyIgPGFyb24yMS0zQHNhZGVtYmUub3JnPsLAdQQQAQgAHwUCXMbOEQYLCQcI
+AwIEFQgKAgMWAgECGQECGwMCHgEACgkQZ/B4v2b2xB6XUgf/dHGRHimyMR78
+QYbEm2cuaEvOtq4a+J6Zv3P4VOWAbvkGWS9LDKSvVi60vq4oYOmF54HgPzur
+nA4OtZDf0HKwQK45VZ7CYD693o70jkKPrAAJG3yTsbesfiS7RbFyGKzKJ7EL
+nsUIJkfgm/SlKmXU/u8MOBO5Wg7/TcsS33sRWHl90j+9jbhqdl92R+vY/CwC
+ieFkQA7/TDv1u+NAalH+Lpkd8AIuEcki+TAogZ7oi/SnofwnoB7BxRm+mIkp
+ZZhIDSCaPOzLG8CSZ81d3HVHhqbf8dh0DFKFoUYyKdbOqIkNWWASf+c/ZEme
+IWcekY8hqwf/raZ56tGM/bRwYPcotMfC1wRcxs4RAQgAsMb5/ELWmrfPy3ba
+5qif+RXhGSbjitATNgHpoPUHrfTC7cn4JWHqehoXLAQpFAoKd+O/ZNpZozK9
+ilpqGUx05yMw06jNQEhYIbgIF4wzPpz02Lp6YeMwdF5LF+Rw83PHdHrA/wRV
+/QjL04+kZnN+G5HmzMlhFY+oZSpL+Gp1bTXgtAVDkhCnMB5tP2VwULMGyJ+X
+vRYxwTK2CrLjIVZv5n1VYY+caCowU6j/XFqvlCJj+G5oV+UhFOWffaMRXhOh
+a64RrhqT1Np7wCLvLMP2wpys9xlMcLQJLqDNxqOTp504V7dm67ncC0fKUsT4
+m4oTktnxKPd6MU+4VYveaLCquwARAQABAAf4u9s7gpGErs1USxmDO9TlyGZK
+aBlri8nMf3s+hOJCOo3cRaRHJBfdY6pu/baG6H6JTsWzeY4MHwr6N+dhVIEh
+FPMa9EZAjagyc4GugxWGiMVTfU+2AEfdrdynhQKMgXSctnnNCdkRuX0nwqb3
+nlupm1hsz2ze4+Wg0BKSLS0FQdoUbITdJUR69OHr4dNJVHWYI0JSBx4SdhV3
+y9163dDvmc+lW9AEaD53vyZWfzCHZxsR/gI32VmT0z5gn1t8w9AOdXo2lA1H
+bf7wh4/qCyujGu64ToZtiEny/GCyM6PofLtiZuJNLw3s/y+B2tKv22aTJ760
++Gib1xB9WcWjKyrxBADoeCyq+nHGrl0CwOkmjanlFymgo7mnBOXuiFOvGrKk
+M1meMU1TI4TEBWkVnDVMcSejgjAf/bX1dtouba1tMAMu7DlaV/0EwbSADRel
+RSqEbIzIOys+y9TY/BMI/uCKNyEKHvu1KUXADb+CBpdBpCfMBWDANFlo9xLz
+Ajcmu2dyawQAwquwC0VXQcvzfs+Hd5au5XvHdm1KidOiAdu6PH1SrOgenIN4
+lkEjHrJD9jmloO2/GVcxDBB2pmf0B4HEg7DuY9LXBrksP5eSbbRc5+UH1HUv
+u82AqQnfNKTd/jae+lLwaOS++ohtwMkkD6W0LdWnHPjyyXg4Oi9zPID3asRu
+3PED/3CYyjl6S8GTMY4FNH7Yxu9+NV2xpKE92Hf8K/hnYlmSSVKDCEeOJtLt
+BkkcSqY6liCNSMmJdVyAF2GrR+zmDac7UQRssf57oOWtSsGozt0aqJXuspMT
+6aB+P1UhZ8Ly9rWZNiJ0jwyfnQNOLCYDaqjFmiSpqrNnJ2Q1Xge3+k80P9DC
+wF8EGAEIAAkFAlzGzhECGwwACgkQZ/B4v2b2xB5wlwgAjZA1zdv5irFjyWVo
+4/itONtyO1NbdpyYpcct7vD0oV+a4wahQP0J3Kk1GhZ5tvAoZF/jakQQOM5o
+GjUYpXAGnr09Mv9EiQ2pDwXc2yq0WfXnGxNrpzOqdtV+IqY9NYkl55Tme7x+
+WRvrkPSUeUsyEGvxwR1stdv8eg9jUmxdl8Io3PYoFJJlrM/6aXeC1r3KOj7q
+XAnR0XHJ+QBSNKCWLlQv5hui9BKfcLiVKFK/dNhs82nRyhPr4sWFw6MTqdAK
+4zkn7l0jmy6Evi1AiiGPiHPnxeNErnofOIEh4REQj00deZADHrixTLtx2FuR
+uaSC3IcBmBsj1fNb4eYXElILjQ==
+=fMOl
+-----END PGP PRIVATE KEY BLOCK-----
\ No newline at end of file
diff --git a/test/fakeapi/messages.go b/test/fakeapi/messages.go
index 3fa76943..557fdf85 100644
--- a/test/fakeapi/messages.go
+++ b/test/fakeapi/messages.go
@@ -20,6 +20,7 @@ package fakeapi
import (
"bytes"
"fmt"
+ "io/ioutil"
"net/mail"
"time"
@@ -67,7 +68,7 @@ func (api *FakePMAPI) ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Messa
for idx := 0; idx < len(api.messages); idx++ {
var message *pmapi.Message
- if !*filter.Desc {
+ if filter.Desc == nil || !*filter.Desc {
message = api.messages[idx]
if filter.BeginID == "" || message.ID == filter.BeginID {
skipByIDBegin = false
@@ -81,7 +82,7 @@ func (api *FakePMAPI) ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Messa
if skipByIDBegin || skipByIDEnd {
continue
}
- if !*filter.Desc {
+ if filter.Desc == nil || !*filter.Desc {
if message.ID == filter.EndID {
skipByIDEnd = true
}
@@ -189,36 +190,60 @@ func (api *FakePMAPI) SendMessage(messageID string, sendMessageRequest *pmapi.Se
}
func (api *FakePMAPI) Import(importMessageRequests []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
+ if err := api.checkAndRecordCall(POST, "/import", importMessageRequests); err != nil {
+ return nil, err
+ }
msgRes := []*pmapi.ImportMsgRes{}
for _, msgReq := range importMessageRequests {
- mailMessage, err := mail.ReadMessage(bytes.NewBuffer(msgReq.Body))
+ message, err := api.generateMessageFromImportRequest(msgReq)
if err != nil {
msgRes = append(msgRes, &pmapi.ImportMsgRes{
Error: err,
})
- }
- messageID := api.controller.messageIDGenerator.next("")
- message := &pmapi.Message{
- ID: messageID,
- AddressID: msgReq.AddressID,
- Sender: &mail.Address{Address: mailMessage.Header.Get("From")},
- ToList: []*mail.Address{{Address: mailMessage.Header.Get("To")}},
- Subject: mailMessage.Header.Get("Subject"),
- Unread: msgReq.Unread,
- LabelIDs: msgReq.LabelIDs,
- Body: string(msgReq.Body),
- Flags: msgReq.Flags,
- Time: msgReq.Time,
+ continue
}
msgRes = append(msgRes, &pmapi.ImportMsgRes{
Error: nil,
- MessageID: messageID,
+ MessageID: message.ID,
})
api.addMessage(message)
}
return msgRes, nil
}
+func (api *FakePMAPI) generateMessageFromImportRequest(msgReq *pmapi.ImportMsgReq) (*pmapi.Message, error) {
+ mailMessage, err := mail.ReadMessage(bytes.NewBuffer(msgReq.Body))
+ if err != nil {
+ return nil, err
+ }
+ body, err := ioutil.ReadAll(mailMessage.Body)
+ if err != nil {
+ return nil, err
+ }
+ sender, err := mail.ParseAddress(mailMessage.Header.Get("From"))
+ if err != nil {
+ return nil, err
+ }
+ toList, err := mail.ParseAddressList(mailMessage.Header.Get("To"))
+ if err != nil {
+ return nil, err
+ }
+ messageID := api.controller.messageIDGenerator.next("")
+ return &pmapi.Message{
+ ID: messageID,
+ AddressID: msgReq.AddressID,
+ Sender: sender,
+ ToList: toList,
+ Subject: mailMessage.Header.Get("Subject"),
+ Unread: msgReq.Unread,
+ LabelIDs: append(msgReq.LabelIDs, pmapi.AllMailLabel),
+ Body: string(body),
+ Header: mailMessage.Header,
+ Flags: msgReq.Flags,
+ Time: msgReq.Time,
+ }, nil
+}
+
func (api *FakePMAPI) addMessage(message *pmapi.Message) {
api.messages = append(api.messages, message)
api.addEventMessage(pmapi.EventCreate, message)
diff --git a/test/features/imap/auth.feature b/test/features/bridge/imap/auth.feature
similarity index 97%
rename from test/features/imap/auth.feature
rename to test/features/bridge/imap/auth.feature
index 8898b50a..3191d1e3 100644
--- a/test/features/imap/auth.feature
+++ b/test/features/bridge/imap/auth.feature
@@ -29,16 +29,16 @@ Feature: IMAP auth
Scenario: Authenticates with freshly logged-out user
Given there is connected user "user"
- When "user" logs out from bridge
+ When "user" logs out
And IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
Scenario: Authenticates user which was re-logged in
Given there is connected user "user"
- When "user" logs out from bridge
+ When "user" logs out
And IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
- When "user" logs in to bridge
+ When "user" logs in
And IMAP client authenticates "user"
Then IMAP response is "OK"
When IMAP client selects "INBOX"
diff --git a/test/features/imap/idle/basic.feature b/test/features/bridge/imap/idle/basic.feature
similarity index 100%
rename from test/features/imap/idle/basic.feature
rename to test/features/bridge/imap/idle/basic.feature
diff --git a/test/features/imap/idle/two_users.feature b/test/features/bridge/imap/idle/two_users.feature
similarity index 100%
rename from test/features/imap/idle/two_users.feature
rename to test/features/bridge/imap/idle/two_users.feature
diff --git a/test/features/imap/mailbox/create.feature b/test/features/bridge/imap/mailbox/create.feature
similarity index 100%
rename from test/features/imap/mailbox/create.feature
rename to test/features/bridge/imap/mailbox/create.feature
diff --git a/test/features/imap/mailbox/delete.feature b/test/features/bridge/imap/mailbox/delete.feature
similarity index 100%
rename from test/features/imap/mailbox/delete.feature
rename to test/features/bridge/imap/mailbox/delete.feature
diff --git a/test/features/imap/mailbox/info.feature b/test/features/bridge/imap/mailbox/info.feature
similarity index 100%
rename from test/features/imap/mailbox/info.feature
rename to test/features/bridge/imap/mailbox/info.feature
diff --git a/test/features/imap/mailbox/list.feature b/test/features/bridge/imap/mailbox/list.feature
similarity index 100%
rename from test/features/imap/mailbox/list.feature
rename to test/features/bridge/imap/mailbox/list.feature
diff --git a/test/features/imap/mailbox/rename.feature b/test/features/bridge/imap/mailbox/rename.feature
similarity index 100%
rename from test/features/imap/mailbox/rename.feature
rename to test/features/bridge/imap/mailbox/rename.feature
diff --git a/test/features/imap/mailbox/select.feature b/test/features/bridge/imap/mailbox/select.feature
similarity index 100%
rename from test/features/imap/mailbox/select.feature
rename to test/features/bridge/imap/mailbox/select.feature
diff --git a/test/features/imap/mailbox/status.feature b/test/features/bridge/imap/mailbox/status.feature
similarity index 100%
rename from test/features/imap/mailbox/status.feature
rename to test/features/bridge/imap/mailbox/status.feature
diff --git a/test/features/imap/message/copy.feature b/test/features/bridge/imap/message/copy.feature
similarity index 100%
rename from test/features/imap/message/copy.feature
rename to test/features/bridge/imap/message/copy.feature
diff --git a/test/features/imap/message/create.feature b/test/features/bridge/imap/message/create.feature
similarity index 100%
rename from test/features/imap/message/create.feature
rename to test/features/bridge/imap/message/create.feature
diff --git a/test/features/imap/message/delete.feature b/test/features/bridge/imap/message/delete.feature
similarity index 100%
rename from test/features/imap/message/delete.feature
rename to test/features/bridge/imap/message/delete.feature
diff --git a/test/features/imap/message/fetch.feature b/test/features/bridge/imap/message/fetch.feature
similarity index 100%
rename from test/features/imap/message/fetch.feature
rename to test/features/bridge/imap/message/fetch.feature
diff --git a/test/features/imap/message/import.feature b/test/features/bridge/imap/message/import.feature
similarity index 100%
rename from test/features/imap/message/import.feature
rename to test/features/bridge/imap/message/import.feature
diff --git a/test/features/imap/message/move.feature b/test/features/bridge/imap/message/move.feature
similarity index 100%
rename from test/features/imap/message/move.feature
rename to test/features/bridge/imap/message/move.feature
diff --git a/test/features/imap/message/search.feature b/test/features/bridge/imap/message/search.feature
similarity index 100%
rename from test/features/imap/message/search.feature
rename to test/features/bridge/imap/message/search.feature
diff --git a/test/features/imap/message/update.feature b/test/features/bridge/imap/message/update.feature
similarity index 100%
rename from test/features/imap/message/update.feature
rename to test/features/bridge/imap/message/update.feature
diff --git a/test/features/smtp/auth.feature b/test/features/bridge/smtp/auth.feature
similarity index 100%
rename from test/features/smtp/auth.feature
rename to test/features/bridge/smtp/auth.feature
diff --git a/test/features/smtp/send/bcc.feature b/test/features/bridge/smtp/send/bcc.feature
similarity index 100%
rename from test/features/smtp/send/bcc.feature
rename to test/features/bridge/smtp/send/bcc.feature
diff --git a/test/features/smtp/send/failures.feature b/test/features/bridge/smtp/send/failures.feature
similarity index 100%
rename from test/features/smtp/send/failures.feature
rename to test/features/bridge/smtp/send/failures.feature
diff --git a/test/features/smtp/send/html.feature b/test/features/bridge/smtp/send/html.feature
similarity index 100%
rename from test/features/smtp/send/html.feature
rename to test/features/bridge/smtp/send/html.feature
diff --git a/test/features/smtp/send/html_att.feature b/test/features/bridge/smtp/send/html_att.feature
similarity index 100%
rename from test/features/smtp/send/html_att.feature
rename to test/features/bridge/smtp/send/html_att.feature
diff --git a/test/features/smtp/send/plain.feature b/test/features/bridge/smtp/send/plain.feature
similarity index 100%
rename from test/features/smtp/send/plain.feature
rename to test/features/bridge/smtp/send/plain.feature
diff --git a/test/features/smtp/send/plain_att.feature b/test/features/bridge/smtp/send/plain_att.feature
similarity index 100%
rename from test/features/smtp/send/plain_att.feature
rename to test/features/bridge/smtp/send/plain_att.feature
diff --git a/test/features/smtp/send/same_message.feature b/test/features/bridge/smtp/send/same_message.feature
similarity index 100%
rename from test/features/smtp/send/same_message.feature
rename to test/features/bridge/smtp/send/same_message.feature
diff --git a/test/features/smtp/send/two_messages.feature b/test/features/bridge/smtp/send/two_messages.feature
similarity index 100%
rename from test/features/smtp/send/two_messages.feature
rename to test/features/bridge/smtp/send/two_messages.feature
diff --git a/test/features/bridge/addressmode.feature b/test/features/bridge/users/addressmode.feature
similarity index 98%
rename from test/features/bridge/addressmode.feature
rename to test/features/bridge/users/addressmode.feature
index f99c4f03..be8e25f8 100644
--- a/test/features/bridge/addressmode.feature
+++ b/test/features/bridge/users/addressmode.feature
@@ -26,7 +26,7 @@ Feature: Address mode
Scenario: Switch address mode from combined to split mode
Given there is "userMoreAddresses" in "combined" address mode
When "userMoreAddresses" changes the address mode
- Then bridge response is "OK"
+ Then last response is "OK"
And "userMoreAddresses" has address mode in "split" mode
And mailbox "Folders/mbox" for address "primary" of "userMoreAddresses" has messages
| from | to | subject |
@@ -38,7 +38,7 @@ Feature: Address mode
Scenario: Switch address mode from split to combined mode
Given there is "userMoreAddresses" in "split" address mode
When "userMoreAddresses" changes the address mode
- Then bridge response is "OK"
+ Then last response is "OK"
And "userMoreAddresses" has address mode in "combined" mode
And mailbox "Folders/mbox" for address "primary" of "userMoreAddresses" has messages
| from | to | subject |
diff --git a/test/features/bridge/deleteuser.feature b/test/features/bridge/users/delete.feature
similarity index 62%
rename from test/features/bridge/deleteuser.feature
rename to test/features/bridge/users/delete.feature
index 61fa6d3b..770ddf6d 100644
--- a/test/features/bridge/deleteuser.feature
+++ b/test/features/bridge/users/delete.feature
@@ -1,36 +1,36 @@
Feature: Delete user
Scenario: Deleting connected user
Given there is connected user "user"
- When user deletes "user" from bridge
- Then bridge response is "OK"
+ When user deletes "user"
+ Then last response is "OK"
And "user" has database file
Scenario: Deleting connected user with cache
Given there is connected user "user"
- When user deletes "user" from bridge with cache
- Then bridge response is "OK"
+ When user deletes "user" with cache
+ Then last response is "OK"
And "user" does not have database file
Scenario: Deleting connected user without database file
Given there is connected user "user"
And there is no database file for "user"
- When user deletes "user" from bridge with cache
- Then bridge response is "OK"
+ When user deletes "user" with cache
+ Then last response is "OK"
Scenario: Deleting disconnected user
Given there is disconnected user "user"
- When user deletes "user" from bridge
- Then bridge response is "OK"
+ When user deletes "user"
+ Then last response is "OK"
And "user" has database file
Scenario: Deleting disconnected user with cache
Given there is disconnected user "user"
- When user deletes "user" from bridge with cache
- Then bridge response is "OK"
+ When user deletes "user" with cache
+ Then last response is "OK"
And "user" does not have database file
Scenario: Deleting disconnected user without database file
Given there is disconnected user "user"
And there is no database file for "user"
- When user deletes "user" from bridge with cache
- Then bridge response is "OK"
+ When user deletes "user" with cache
+ Then last response is "OK"
diff --git a/test/features/bridge/login.feature b/test/features/bridge/users/login.feature
similarity index 59%
rename from test/features/bridge/login.feature
rename to test/features/bridge/users/login.feature
index a74cd1d1..8f89bee1 100644
--- a/test/features/bridge/login.feature
+++ b/test/features/bridge/users/login.feature
@@ -1,47 +1,47 @@
-Feature: Login to bridge for the first time
- Scenario: Normal bridge login
+Feature: Login for the first time
+ Scenario: Normal login
Given there is user "user"
- When "user" logs in to bridge
- Then bridge response is "OK"
+ When "user" logs in
+ Then last response is "OK"
And "user" is connected
And "user" has database file
And "user" has running event loop
Scenario: Login with bad username
- When "user" logs in to bridge with bad password
- Then bridge response is "failed to login: Incorrect login credentials. Please try again"
+ When "user" logs in with bad password
+ Then last response is "failed to login: Incorrect login credentials. Please try again"
Scenario: Login with bad password
Given there is user "user"
- When "user" logs in to bridge with bad password
- Then bridge response is "failed to login: Incorrect login credentials. Please try again"
+ When "user" logs in with bad password
+ Then last response is "failed to login: Incorrect login credentials. Please try again"
Scenario: Login without internet connection
Given there is no internet connection
- When "user" logs in to bridge
- Then bridge response is "failed to login: cannot reach the server"
+ When "user" logs in
+ Then last response is "failed to login: cannot reach the server"
@ignore-live
Scenario: Login user with 2FA
Given there is user "user2fa"
- When "user2fa" logs in to bridge
- Then bridge response is "OK"
+ When "user2fa" logs in
+ Then last response is "OK"
And "user2fa" is connected
And "user2fa" has database file
And "user2fa" has running event loop
Scenario: Login user with capital letters in address
Given there is user "userAddressWithCapitalLetter"
- When "userAddressWithCapitalLetter" logs in to bridge
- Then bridge response is "OK"
+ When "userAddressWithCapitalLetter" logs in
+ Then last response is "OK"
And "userAddressWithCapitalLetter" is connected
And "userAddressWithCapitalLetter" has database file
And "userAddressWithCapitalLetter" has running event loop
Scenario: Login user with more addresses
Given there is user "userMoreAddresses"
- When "userMoreAddresses" logs in to bridge
- Then bridge response is "OK"
+ When "userMoreAddresses" logs in
+ Then last response is "OK"
And "userMoreAddresses" is connected
And "userMoreAddresses" has database file
And "userMoreAddresses" has running event loop
@@ -49,8 +49,8 @@ Feature: Login to bridge for the first time
@ignore-live
Scenario: Login user with disabled primary address
Given there is user "userDisabledPrimaryAddress"
- When "userDisabledPrimaryAddress" logs in to bridge
- Then bridge response is "OK"
+ When "userDisabledPrimaryAddress" logs in
+ Then last response is "OK"
And "userDisabledPrimaryAddress" is connected
And "userDisabledPrimaryAddress" has database file
And "userDisabledPrimaryAddress" has running event loop
@@ -58,9 +58,9 @@ Feature: Login to bridge for the first time
Scenario: Login two users
Given there is user "user"
And there is user "userMoreAddresses"
- When "user" logs in to bridge
- Then bridge response is "OK"
+ When "user" logs in
+ Then last response is "OK"
And "user" is connected
- When "userMoreAddresses" logs in to bridge
- Then bridge response is "OK"
+ When "userMoreAddresses" logs in
+ Then last response is "OK"
And "userMoreAddresses" is connected
diff --git a/test/features/bridge/relogin.feature b/test/features/bridge/users/relogin.feature
similarity index 70%
rename from test/features/bridge/relogin.feature
rename to test/features/bridge/users/relogin.feature
index aeb4dd06..e0b43d79 100644
--- a/test/features/bridge/relogin.feature
+++ b/test/features/bridge/users/relogin.feature
@@ -1,9 +1,9 @@
-Feature: Re-login to bridge
+Feature: Re-login
Scenario: Re-login with connected user and database file
Given there is connected user "user"
And there is database file for "user"
- When "user" logs in to bridge
- Then bridge response is "failed to finish login: user is already connected"
+ When "user" logs in
+ Then last response is "failed to finish login: user is already connected"
And "user" is connected
And "user" has running event loop
@@ -11,8 +11,8 @@ Feature: Re-login to bridge
Scenario: Re-login with connected user and no database file
Given there is connected user "user"
And there is no database file for "user"
- When "user" logs in to bridge
- Then bridge response is "failed to finish login: user is already connected"
+ When "user" logs in
+ Then last response is "failed to finish login: user is already connected"
And "user" is connected
And "user" has database file
And "user" has running event loop
@@ -20,16 +20,16 @@ Feature: Re-login to bridge
Scenario: Re-login with disconnected user and database file
Given there is disconnected user "user"
And there is database file for "user"
- When "user" logs in to bridge
- Then bridge response is "OK"
+ When "user" logs in
+ Then last response is "OK"
And "user" is connected
And "user" has running event loop
Scenario: Re-login with disconnected user and no database file
Given there is disconnected user "user"
And there is no database file for "user"
- When "user" logs in to bridge
- Then bridge response is "OK"
+ When "user" logs in
+ Then last response is "OK"
And "user" is connected
And "user" has database file
And "user" has running event loop
diff --git a/test/features/bridge/sync.feature b/test/features/bridge/users/sync.feature
similarity index 97%
rename from test/features/bridge/sync.feature
rename to test/features/bridge/users/sync.feature
index 6824200d..09c616d8 100644
--- a/test/features/bridge/sync.feature
+++ b/test/features/bridge/users/sync.feature
@@ -28,7 +28,7 @@ Feature: Sync bridge
Scenario: Sync in combined mode
And there is "userMoreAddresses" in "combined" address mode
When bridge syncs "userMoreAddresses"
- Then bridge response is "OK"
+ Then last response is "OK"
And "userMoreAddresses" has the following messages
| mailboxes | messages |
| INBOX | 1101 |
@@ -43,7 +43,7 @@ Feature: Sync bridge
Scenario: Sync in split mode
And there is "userMoreAddresses" in "split" address mode
When bridge syncs "userMoreAddresses"
- Then bridge response is "OK"
+ Then last response is "OK"
And "userMoreAddresses" has the following messages
| address | mailboxes | messages |
| primary | INBOX | 1001 |
diff --git a/test/features/ie/transfer/export_eml.feature b/test/features/ie/transfer/export_eml.feature
new file mode 100644
index 00000000..50a7917c
--- /dev/null
+++ b/test/features/ie/transfer/export_eml.feature
@@ -0,0 +1,43 @@
+Feature: Export to EML files
+ Background:
+ Given there is connected user "user"
+ And there is "user" with mailbox "Folders/Foo"
+ And there are messages in mailbox "INBOX" for "user"
+ | from | to | subject | time |
+ | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
+ And there are messages in mailbox "Folders/Foo" for "user"
+ | from | to | subject | time |
+ | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+
+ Scenario: Export all
+ When user "user" exports to EML files
+ Then progress result is "OK"
+ # Every message is also in All Mail.
+ And transfer exported 8 messages
+ And transfer imported 8 messages
+ And transfer failed for 0 messages
+ And transfer exported messages
+ | folder | from | to | subject | time |
+ | Inbox | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
+ | Foo | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | Foo | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | Foo | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+ | All Mail | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
+ | All Mail | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | All Mail | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | All Mail | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+
+ Scenario: Export only Foo with time limit
+ When user "user" exports to EML files with rules
+ | source | target | from | to |
+ | Foo | | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
+ Then progress result is "OK"
+ And transfer exported 2 messages
+ And transfer imported 2 messages
+ And transfer failed for 0 messages
+ And transfer exported messages
+ | folder | from | to | subject | time |
+ | Foo | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | Foo | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
diff --git a/test/features/ie/transfer/export_mbox.feature b/test/features/ie/transfer/export_mbox.feature
new file mode 100644
index 00000000..a0143eed
--- /dev/null
+++ b/test/features/ie/transfer/export_mbox.feature
@@ -0,0 +1,43 @@
+Feature: Export to MBOX files
+ Background:
+ Given there is connected user "user"
+ And there is "user" with mailbox "Folders/Foo"
+ And there are messages in mailbox "INBOX" for "user"
+ | from | to | subject | time |
+ | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
+ And there are messages in mailbox "Folders/Foo" for "user"
+ | from | to | subject | time |
+ | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+
+ Scenario: Export all
+ When user "user" exports to MBOX files
+ Then progress result is "OK"
+ # Every message is also in All Mail.
+ And transfer exported 8 messages
+ And transfer imported 8 messages
+ And transfer failed for 0 messages
+ And transfer exported messages
+ | folder | from | to | subject | time |
+ | Inbox | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
+ | Foo | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | Foo | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | Foo | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+ | All Mail | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
+ | All Mail | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | All Mail | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | All Mail | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+
+ Scenario: Export only Foo with time limit
+ When user "user" exports to MBOX files with rules
+ | source | target | from | to |
+ | Foo | | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
+ Then progress result is "OK"
+ And transfer exported 2 messages
+ And transfer imported 2 messages
+ And transfer failed for 0 messages
+ And transfer exported messages
+ | folder | from | to | subject | time |
+ | Foo | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | Foo | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
diff --git a/test/features/ie/transfer/import_eml.feature b/test/features/ie/transfer/import_eml.feature
new file mode 100644
index 00000000..5bd1cae9
--- /dev/null
+++ b/test/features/ie/transfer/import_eml.feature
@@ -0,0 +1,60 @@
+Feature: Import from EML files
+ Background:
+ Given there is connected user "user"
+ And there is "user" with mailbox "Folders/Foo"
+ And there is "user" with mailbox "Folders/Bar"
+ And there are EML files
+ | file | from | to | subject | time |
+ | Foo/one.eml | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | Foo/two.eml | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | Sub/Foo/three.eml | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+ And there is EML file "Inbox/hello.eml"
+ """
+ Subject: hello
+ From: Bridge Test
+ To: Internal Bridge
+
+ hello
+
+ """
+
+ Scenario: Import all
+ When user "user" imports local files
+ Then progress result is "OK"
+ And transfer exported 4 messages
+ And transfer imported 4 messages
+ And transfer failed for 0 messages
+ And API mailbox "INBOX" for "user" has messages
+ | from | to | subject |
+ | bridgetest@pm.test | test@protonmail.com | hello |
+ And API mailbox "Folders/Foo" for "user" has messages
+ | from | to | subject |
+ | foo@example.com | bridgetest@protonmail.com | one |
+ | bar@example.com | bridgetest@protonmail.com | two |
+ | bar@example.com | bridgetest@protonmail.com | three |
+
+ Scenario: Import only Foo to Bar with time limit
+ When user "user" imports local files with rules
+ | source | target | from | to |
+ | Foo | Bar | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
+ Then progress result is "OK"
+ And transfer exported 2 messages
+ And transfer imported 2 messages
+ And transfer failed for 0 messages
+ And API mailbox "Folders/Bar" for "user" has messages
+ | from | to | subject |
+ | bar@example.com | bridgetest@protonmail.com | two |
+ | bar@example.com | bridgetest@protonmail.com | three |
+
+ Scenario: Import broken EML message
+ Given there is EML file "Broken/broken.eml"
+ """
+ Content-type: image/png
+ """
+ When user "user" imports local files with rules
+ | source | target |
+ | Broken | Foo |
+ Then progress result is "OK"
+ And transfer exported 1 messages
+ And transfer imported 0 messages
+ And transfer failed for 1 messages
diff --git a/test/features/ie/transfer/import_export.feature b/test/features/ie/transfer/import_export.feature
new file mode 100644
index 00000000..5cd5d29e
--- /dev/null
+++ b/test/features/ie/transfer/import_export.feature
@@ -0,0 +1,49 @@
+Feature: Import/Export
+ Background:
+ Given there is connected user "user"
+ And there is "user" with mailbox "Folders/Foo"
+ And there is "user" with mailbox "Folders/Bar"
+
+ Scenario: EML -> PM -> EML
+ Given there are EML files
+ | file | from | to | subject | time |
+ | Inbox/hello.eml | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
+ | Foo/one.eml | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | Foo/two.eml | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | Sub/Foo/three.eml | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+
+ When user "user" imports local files
+ Then progress result is "OK"
+ And transfer failed for 0 messages
+ And transfer imported 4 messages
+
+ When user "user" exports to EML files
+ Then progress result is "OK"
+ And transfer failed for 0 messages
+ # Every message is also in All Mail.
+ And transfer imported 8 messages
+
+ And exported messages match the original ones
+
+ Scenario: MBOX -> PM -> MBOX
+ Given there is MBOX file "Inbox.mbox" with messages
+ | from | to | subject | time |
+ | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
+ And there is MBOX file "Foo.mbox" with messages
+ | from | to | subject | time |
+ | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+
+ When user "user" imports local files
+ Then progress result is "OK"
+ And transfer failed for 0 messages
+ And transfer imported 4 messages
+
+ When user "user" exports to MBOX files
+ Then progress result is "OK"
+ And transfer failed for 0 messages
+ # Every message is also in All Mail.
+ And transfer imported 8 messages
+
+ And exported messages match the original ones
diff --git a/test/features/ie/transfer/import_imap.feature b/test/features/ie/transfer/import_imap.feature
new file mode 100644
index 00000000..f3766348
--- /dev/null
+++ b/test/features/ie/transfer/import_imap.feature
@@ -0,0 +1,79 @@
+Feature: Import from IMAP server
+ Background:
+ Given there is connected user "user"
+ And there is "user" with mailbox "Folders/Foo"
+ And there is "user" with mailbox "Folders/Bar"
+ And there are IMAP mailboxes
+ | name |
+ | Inbox |
+ | Foo |
+ | Broken |
+ And there are IMAP messages
+ | mailbox | seqnum | uid | from | to | subject | time |
+ | Foo | 1 | 12 | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | Foo | 2 | 14 | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ | Foo | 3 | 15 | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+ And there is IMAP message in mailbox "Inbox" with seq 1, uid 42, time "2020-01-01T12:34:56" and subject "hello"
+ """
+ Subject: hello
+ From: Bridge Test
+ To: Internal Bridge
+
+ hello
+
+ """
+
+ Scenario: Import all
+ When user "user" imports remote messages
+ Then progress result is "OK"
+ And transfer exported 4 messages
+ And transfer imported 4 messages
+ And transfer failed for 0 messages
+ And API mailbox "INBOX" for "user" has messages
+ | from | to | subject |
+ | bridgetest@pm.test | test@protonmail.com | hello |
+ And API mailbox "Folders/Foo" for "user" has messages
+ | from | to | subject |
+ | foo@example.com | bridgetest@protonmail.com | one |
+ | bar@example.com | bridgetest@protonmail.com | two |
+ | bar@example.com | bridgetest@protonmail.com | three |
+
+ Scenario: Import only Foo to Bar with time limit
+ When user "user" imports remote messages with rules
+ | source | target | from | to |
+ | Foo | Bar | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
+ Then progress result is "OK"
+ And transfer exported 2 messages
+ And transfer imported 2 messages
+ And transfer failed for 0 messages
+ And API mailbox "Folders/Bar" for "user" has messages
+ | from | to | subject |
+ | bar@example.com | bridgetest@protonmail.com | two |
+ | bar@example.com | bridgetest@protonmail.com | three |
+
+ # Note we need to have message which we can parse and use in go-imap
+ # but which has problem on our side. Used example with missing boundary
+ # is real example which we want to solve one day. Probabl this test
+ # can be removed once we import any time of message or switch is to
+ # something we will never allow.
+ Scenario: Import broken message
+ Given there is IMAP message in mailbox "Broken" with seq 1, uid 42, time "2020-01-01T12:34:56" and subject "broken"
+ """
+ Subject: missing boundary end
+ Content-Type: multipart/related; boundary=boundary
+
+ --boundary
+ Content-Disposition: inline
+ Content-Transfer-Encoding: quoted-printable
+ Content-Type: text/plain; charset=utf-8
+
+ body
+
+ """
+ When user "user" imports remote messages with rules
+ | source | target |
+ | Broken | Foo |
+ Then progress result is "OK"
+ And transfer exported 1 messages
+ And transfer imported 0 messages
+ And transfer failed for 1 messages
diff --git a/test/features/ie/transfer/import_mbox.feature b/test/features/ie/transfer/import_mbox.feature
new file mode 100644
index 00000000..90388992
--- /dev/null
+++ b/test/features/ie/transfer/import_mbox.feature
@@ -0,0 +1,64 @@
+Feature: Import from MBOX files
+ Background:
+ Given there is connected user "user"
+ And there is "user" with mailbox "Folders/Foo"
+ And there is "user" with mailbox "Folders/Bar"
+ And there is MBOX file "Foo.mbox" with messages
+ | from | to | subject | time |
+ | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
+ | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
+ And there is MBOX file "Sub/Foo.mbox" with messages
+ | from | to | subject | time |
+ | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
+ And there is MBOX file "Inbox.mbox"
+ """
+ From bridgetest@pm.test Thu Feb 20 20:20:20 2020
+ Subject: hello
+ From: Bridge Test
+ To: Internal Bridge
+
+ hello
+
+ """
+
+ Scenario: Import all
+ When user "user" imports local files
+ Then progress result is "OK"
+ And transfer exported 4 messages
+ And transfer imported 4 messages
+ And transfer failed for 0 messages
+ And API mailbox "INBOX" for "user" has messages
+ | from | to | subject |
+ | bridgetest@pm.test | test@protonmail.com | hello |
+ And API mailbox "Folders/Foo" for "user" has messages
+ | from | to | subject |
+ | foo@example.com | bridgetest@protonmail.com | one |
+ | bar@example.com | bridgetest@protonmail.com | two |
+ | bar@example.com | bridgetest@protonmail.com | three |
+
+ Scenario: Import only Foo to Bar with time limit
+ When user "user" imports local files with rules
+ | source | target | from | to |
+ | Foo | Bar | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
+ Then progress result is "OK"
+ And transfer exported 2 messages
+ And transfer imported 2 messages
+ And transfer failed for 0 messages
+ And API mailbox "Folders/Bar" for "user" has messages
+ | from | to | subject |
+ | bar@example.com | bridgetest@protonmail.com | two |
+ | bar@example.com | bridgetest@protonmail.com | three |
+
+ Scenario: Import broken message
+ Given there is MBOX file "Broken.mbox"
+ """
+ From bridgetest@pm.test Thu Feb 20 20:20:20 2020
+ Content-type: image/png
+ """
+ When user "user" imports local files with rules
+ | source | target |
+ | Broken | Foo |
+ Then progress result is "OK"
+ And transfer exported 1 messages
+ And transfer imported 0 messages
+ And transfer failed for 1 messages
diff --git a/test/features/ie/users/delete.feature b/test/features/ie/users/delete.feature
new file mode 100644
index 00000000..34ffe4b0
--- /dev/null
+++ b/test/features/ie/users/delete.feature
@@ -0,0 +1,20 @@
+Feature: Delete user
+ Scenario: Deleting connected user
+ Given there is connected user "user"
+ When user deletes "user"
+ Then last response is "OK"
+
+ Scenario: Deleting connected user with cache
+ Given there is connected user "user"
+ When user deletes "user" with cache
+ Then last response is "OK"
+
+ Scenario: Deleting disconnected user
+ Given there is disconnected user "user"
+ When user deletes "user"
+ Then last response is "OK"
+
+ Scenario: Deleting disconnected user with cache
+ Given there is disconnected user "user"
+ When user deletes "user" with cache
+ Then last response is "OK"
diff --git a/test/features/ie/users/login.feature b/test/features/ie/users/login.feature
new file mode 100644
index 00000000..e74cc7b2
--- /dev/null
+++ b/test/features/ie/users/login.feature
@@ -0,0 +1,56 @@
+Feature: Login for the first time
+ Scenario: Normal login
+ Given there is user "user"
+ When "user" logs in
+ Then last response is "OK"
+ And "user" is connected
+
+ Scenario: Login with bad username
+ When "user" logs in with bad password
+ Then last response is "failed to login: Incorrect login credentials. Please try again"
+
+ Scenario: Login with bad password
+ Given there is user "user"
+ When "user" logs in with bad password
+ Then last response is "failed to login: Incorrect login credentials. Please try again"
+
+ Scenario: Login without internet connection
+ Given there is no internet connection
+ When "user" logs in
+ Then last response is "failed to login: cannot reach the server"
+
+ @ignore-live
+ Scenario: Login user with 2FA
+ Given there is user "user2fa"
+ When "user2fa" logs in
+ Then last response is "OK"
+ And "user2fa" is connected
+
+ Scenario: Login user with capital letters in address
+ Given there is user "userAddressWithCapitalLetter"
+ When "userAddressWithCapitalLetter" logs in
+ Then last response is "OK"
+ And "userAddressWithCapitalLetter" is connected
+
+ Scenario: Login user with more addresses
+ Given there is user "userMoreAddresses"
+ When "userMoreAddresses" logs in
+ Then last response is "OK"
+ And "userMoreAddresses" is connected
+
+ @ignore-live
+ Scenario: Login user with disabled primary address
+ Given there is user "userDisabledPrimaryAddress"
+ When "userDisabledPrimaryAddress" logs in
+ Then last response is "OK"
+ And "userDisabledPrimaryAddress" is connected
+
+ Scenario: Login two users
+ Given there is user "user"
+ And there is user "userMoreAddresses"
+ When "user" logs in
+ Then last response is "OK"
+ And "user" is connected
+ When "userMoreAddresses" logs in
+ Then last response is "OK"
+ And "userMoreAddresses" is connected
diff --git a/test/features/ie/users/relogin.feature b/test/features/ie/users/relogin.feature
new file mode 100644
index 00000000..d55e3eea
--- /dev/null
+++ b/test/features/ie/users/relogin.feature
@@ -0,0 +1,12 @@
+Feature: Re-login
+ Scenario: Re-login with connected user
+ Given there is connected user "user"
+ When "user" logs in
+ Then last response is "failed to finish login: user is already connected"
+ And "user" is connected
+
+ Scenario: Re-login with disconnected user
+ Given there is disconnected user "user"
+ When "user" logs in
+ Then last response is "OK"
+ And "user" is connected
diff --git a/test/liveapi/messages.go b/test/liveapi/messages.go
index f064cffd..1d682aaa 100644
--- a/test/liveapi/messages.go
+++ b/test/liveapi/messages.go
@@ -135,3 +135,31 @@ func (ctl *Controller) GetMessageID(username, messageIndex string) string {
}
return ctl.messageIDsByUsername[username][idx-1]
}
+
+func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message, error) {
+ client, ok := ctl.pmapiByUsername[username]
+ if !ok {
+ return nil, fmt.Errorf("user %s does not exist", username)
+ }
+
+ page := 0
+ messages := []*pmapi.Message{}
+
+ for {
+ // ListMessages returns empty result, not error, asking for page out of range.
+ pageMessages, _, err := client.ListMessages(&pmapi.MessagesFilter{
+ Page: page,
+ PageSize: 150,
+ LabelID: labelID,
+ })
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to list messages")
+ }
+ messages = append(messages, pageMessages...)
+ if len(pageMessages) < 150 {
+ break
+ }
+ }
+
+ return messages, nil
+}
diff --git a/test/mocks/imap.go b/test/mocks/imap_client.go
similarity index 100%
rename from test/mocks/imap.go
rename to test/mocks/imap_client.go
diff --git a/test/mocks/imap_response.go b/test/mocks/imap_response.go
index 75c36e7c..5945f405 100644
--- a/test/mocks/imap_response.go
+++ b/test/mocks/imap_response.go
@@ -18,13 +18,13 @@
package mocks
import (
- "bufio"
"fmt"
"io"
"regexp"
"strings"
"time"
+ "github.com/emersion/go-imap"
"github.com/pkg/errors"
a "github.com/stretchr/testify/assert"
)
@@ -37,7 +37,7 @@ type IMAPResponse struct {
done bool
}
-func (ir *IMAPResponse) sendCommand(reqTag string, reqIndex int, command string, debug *debug, conn io.Writer, response *bufio.Reader) {
+func (ir *IMAPResponse) sendCommand(reqTag string, reqIndex int, command string, debug *debug, conn io.Writer, response imap.StringReader) {
defer func() { ir.done = true }()
tstart := time.Now()
diff --git a/test/mocks/imap_server.go b/test/mocks/imap_server.go
new file mode 100644
index 00000000..b47f5ce2
--- /dev/null
+++ b/test/mocks/imap_server.go
@@ -0,0 +1,227 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package mocks
+
+import (
+ "fmt"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/emersion/go-imap"
+ imapbackend "github.com/emersion/go-imap/backend"
+ imapserver "github.com/emersion/go-imap/server"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+type IMAPServer struct {
+ Username string
+ Password string
+ Host string
+ Port string
+
+ mailboxes []string
+ messages map[string][]*imap.Message // Key is mailbox.
+ server *imapserver.Server
+}
+
+func NewIMAPServer(username, password, host, port string) *IMAPServer {
+ return &IMAPServer{
+ Username: username,
+ Password: password,
+ Host: host,
+ Port: port,
+
+ mailboxes: []string{},
+ messages: map[string][]*imap.Message{},
+ }
+}
+
+func (s *IMAPServer) AddMailbox(mailboxName string) {
+ s.mailboxes = append(s.mailboxes, mailboxName)
+ s.messages[strings.ToLower(mailboxName)] = []*imap.Message{}
+}
+
+func (s *IMAPServer) AddMessage(mailboxName string, message *imap.Message) {
+ mailboxName = strings.ToLower(mailboxName)
+ s.messages[mailboxName] = append(s.messages[mailboxName], message)
+}
+
+func (s *IMAPServer) Start() {
+ server := imapserver.New(&IMAPBackend{server: s})
+ server.Addr = net.JoinHostPort(s.Host, s.Port)
+ server.AllowInsecureAuth = true
+ server.ErrorLog = logrus.WithField("pkg", "imap-server")
+ server.Debug = logrus.WithField("pkg", "imap-server").WriterLevel(logrus.DebugLevel)
+ server.AutoLogout = 30 * time.Minute
+
+ s.server = server
+
+ go func() {
+ err := server.ListenAndServe()
+ logrus.WithError(err).Warn("IMAP server stopped")
+ }()
+
+ time.Sleep(100 * time.Millisecond)
+}
+
+func (s *IMAPServer) Stop() {
+ _ = s.server.Close()
+}
+
+type IMAPBackend struct {
+ server *IMAPServer
+}
+
+func (b *IMAPBackend) Login(connInfo *imap.ConnInfo, username, password string) (imapbackend.User, error) {
+ if username != b.server.Username || password != b.server.Password {
+ return nil, errors.New("invalid credentials")
+ }
+ return &IMAPUser{
+ server: b.server,
+ username: username,
+ }, nil
+}
+
+type IMAPUser struct {
+ server *IMAPServer
+ username string
+}
+
+func (u *IMAPUser) Username() string {
+ return u.username
+}
+
+func (u *IMAPUser) ListMailboxes(subscribed bool) ([]imapbackend.Mailbox, error) {
+ mailboxes := []imapbackend.Mailbox{}
+ for _, mailboxName := range u.server.mailboxes {
+ mailboxes = append(mailboxes, &IMAPMailbox{
+ server: u.server,
+ name: mailboxName,
+ })
+ }
+ return mailboxes, nil
+}
+
+func (u *IMAPUser) GetMailbox(name string) (imapbackend.Mailbox, error) {
+ name = strings.ToLower(name)
+ _, ok := u.server.messages[name]
+ if !ok {
+ return nil, fmt.Errorf("mailbox %s not found", name)
+ }
+ return &IMAPMailbox{
+ server: u.server,
+ name: name,
+ }, nil
+}
+
+func (u *IMAPUser) CreateMailbox(name string) error {
+ return errors.New("not supported: create mailbox")
+}
+
+func (u *IMAPUser) DeleteMailbox(name string) error {
+ return errors.New("not supported: delete mailbox")
+}
+
+func (u *IMAPUser) RenameMailbox(existingName, newName string) error {
+ return errors.New("not supported: rename mailbox")
+}
+
+func (u *IMAPUser) Logout() error {
+ return nil
+}
+
+type IMAPMailbox struct {
+ server *IMAPServer
+ name string
+ attributes []string
+}
+
+func (m *IMAPMailbox) Name() string {
+ return m.name
+}
+
+func (m *IMAPMailbox) Info() (*imap.MailboxInfo, error) {
+ return &imap.MailboxInfo{
+ Name: m.name,
+ Attributes: m.attributes,
+ }, nil
+}
+
+func (m *IMAPMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
+ status := imap.NewMailboxStatus(m.name, items)
+ status.UidValidity = 1
+ status.Messages = uint32(len(m.server.messages[m.name]))
+ return status, nil
+}
+
+func (m *IMAPMailbox) SetSubscribed(subscribed bool) error {
+ return errors.New("not supported: set subscribed")
+}
+
+func (m *IMAPMailbox) Check() error {
+ return errors.New("not supported: check")
+}
+
+func (m *IMAPMailbox) ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error {
+ defer func() {
+ close(ch)
+ }()
+
+ for index, message := range m.server.messages[m.name] {
+ seqNum := uint32(index + 1)
+ var id uint32
+ if uid {
+ id = message.Uid
+ } else {
+ id = seqNum
+ }
+ if seqset.Contains(id) {
+ msg := imap.NewMessage(seqNum, items)
+ msg.Envelope = message.Envelope
+ msg.BodyStructure = message.BodyStructure
+ msg.Body = message.Body
+ msg.Size = message.Size
+ msg.Uid = message.Uid
+
+ ch <- msg
+ }
+ }
+ return nil
+}
+
+func (m *IMAPMailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) {
+ return nil, errors.New("not supported: search")
+}
+
+func (m *IMAPMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
+ return errors.New("not supported: create")
+}
+
+func (m *IMAPMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, flags []string) error {
+ return errors.New("not supported: update flags")
+}
+
+func (m *IMAPMailbox) CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error {
+ return errors.New("not supported: copy")
+}
+
+func (m *IMAPMailbox) Expunge() error {
+ return errors.New("not supported: expunge")
+}
diff --git a/test/store_checks_test.go b/test/store_checks_test.go
index f90ea344..bb5aebcd 100644
--- a/test/store_checks_test.go
+++ b/test/store_checks_test.go
@@ -172,12 +172,12 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []*pm
matches := true
for n, cell := range row.Cells {
switch head[n].Value {
- case "from":
+ case "from": //nolint[goconst]
address := ctx.EnsureAddress(account.Username(), cell.Value)
if !areAddressesSame(message.Sender.Address, address) {
matches = false
}
- case "to":
+ case "to": //nolint[goconst]
for _, address := range strings.Split(cell.Value, ",") {
address = ctx.EnsureAddress(account.Username(), address)
for _, to := range message.ToList {
@@ -197,7 +197,7 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []*pm
}
}
}
- case "subject":
+ case "subject": //nolint[goconst]
expectedSubject := cell.Value
if expectedSubject == "" {
expectedSubject = "(No Subject)"
@@ -205,7 +205,7 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []*pm
if message.Subject != expectedSubject {
matches = false
}
- case "body":
+ case "body": //nolint[goconst]
if message.Body != cell.Value {
matches = false
}
@@ -238,7 +238,7 @@ func areAddressesSame(first, second string) bool {
if err != nil {
return false
}
- return firstAddress.String() == secondAddress.String()
+ return firstAddress.Address == secondAddress.Address
}
func messagesInMailboxForUserIsMarkedAsRead(messageIDs, mailboxName, bddUserID string) error {
diff --git a/test/store_setup_test.go b/test/store_setup_test.go
index 9db508e0..ff456c36 100644
--- a/test/store_setup_test.go
+++ b/test/store_setup_test.go
@@ -22,6 +22,7 @@ import (
"net/mail"
"strconv"
"strings"
+ "time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/cucumber/godog"
@@ -63,6 +64,9 @@ func thereIsUserWithMailbox(bddUserID, mailboxName string) error {
if err != nil {
return internalError(err, "getting store of %s", account.Username())
}
+ if store == nil {
+ return nil
+ }
return internalError(store.RebuildMailboxes(), "rebuilding mailboxes")
}
@@ -122,6 +126,12 @@ func thereAreMessagesInMailboxesForAddressOfUser(mailboxNames, bddAddressID, bdd
if cell.Value == "true" {
message.LabelIDs = append(message.LabelIDs, "10")
}
+ case "time": //nolint[goconst]
+ date, err := time.Parse(timeFormat, cell.Value)
+ if err != nil {
+ return internalError(err, "parsing time")
+ }
+ message.Time = date.Unix()
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
diff --git a/test/transfer_actions_test.go b/test/transfer_actions_test.go
new file mode 100644
index 00000000..ae049c2e
--- /dev/null
+++ b/test/transfer_actions_test.go
@@ -0,0 +1,227 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package tests
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/ProtonMail/proton-bridge/internal/transfer"
+ "github.com/cucumber/godog"
+ "github.com/cucumber/godog/gherkin"
+)
+
+func TransferActionsFeatureContext(s *godog.Suite) {
+ s.Step(`^user "([^"]*)" imports local files$`, userImportsLocalFiles)
+ s.Step(`^user "([^"]*)" imports local files with rules$`, userImportsLocalFilesWithRules)
+ s.Step(`^user "([^"]*)" imports local files to address "([^"]*)"$`, userImportsLocalFilesToAddress)
+ s.Step(`^user "([^"]*)" imports local files to address "([^"]*)" with rules$`, userImportsLocalFilesToAddressWithRules)
+ s.Step(`^user "([^"]*)" imports remote messages$`, userImportsRemoteMessages)
+ s.Step(`^user "([^"]*)" imports remote messages with rules$`, userImportsRemoteMessagesWithRules)
+ s.Step(`^user "([^"]*)" imports remote messages to address "([^"]*)"$`, userImportsRemoteMessagesToAddress)
+ s.Step(`^user "([^"]*)" imports remote messages to address "([^"]*)" with rules$`, userImportsRemoteMessagesToAddressWithRules)
+ s.Step(`^user "([^"]*)" exports to EML files$`, userExportsToEMLFiles)
+ s.Step(`^user "([^"]*)" exports to EML files with rules$`, userExportsToEMLFilesWithRules)
+ s.Step(`^user "([^"]*)" exports address "([^"]*)" to EML files$`, userExportsAddressToEMLFiles)
+ s.Step(`^user "([^"]*)" exports address "([^"]*)" to EML files with rules$`, userExportsAddressToEMLFilesWithRules)
+ s.Step(`^user "([^"]*)" exports to MBOX files$`, userExportsToMBOXFiles)
+ s.Step(`^user "([^"]*)" exports to MBOX files with rules$`, userExportsToMBOXFilesWithRules)
+ s.Step(`^user "([^"]*)" exports address "([^"]*)" to MBOX files$`, userExportsAddressToMBOXFiles)
+ s.Step(`^user "([^"]*)" exports address "([^"]*)" to MBOX files with rules$`, userExportsAddressToMBOXFilesWithRules)
+}
+
+// Local import.
+
+func userImportsLocalFiles(bddUserID string) error {
+ return userImportsLocalFilesToAddressWithRules(bddUserID, "", nil)
+}
+
+func userImportsLocalFilesWithRules(bddUserID string, rules *gherkin.DataTable) error {
+ return userImportsLocalFilesToAddressWithRules(bddUserID, "", rules)
+}
+
+func userImportsLocalFilesToAddress(bddUserID, bddAddressID string) error {
+ return userImportsLocalFilesToAddressWithRules(bddUserID, bddAddressID, nil)
+}
+
+func userImportsLocalFilesToAddressWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error {
+ return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) {
+ path := ctx.GetTransferLocalRootForImport()
+ return ctx.GetImportExport().GetLocalImporter(address, path)
+ })
+}
+
+// Remote import.
+
+func userImportsRemoteMessages(bddUserID string) error {
+ return userImportsRemoteMessagesToAddressWithRules(bddUserID, "", nil)
+}
+
+func userImportsRemoteMessagesWithRules(bddUserID string, rules *gherkin.DataTable) error {
+ return userImportsRemoteMessagesToAddressWithRules(bddUserID, "", rules)
+}
+
+func userImportsRemoteMessagesToAddress(bddUserID, bddAddressID string) error {
+ return userImportsRemoteMessagesToAddressWithRules(bddUserID, bddAddressID, nil)
+}
+
+func userImportsRemoteMessagesToAddressWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error {
+ return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) {
+ imapServer := ctx.GetTransferRemoteIMAPServer()
+ return ctx.GetImportExport().GetRemoteImporter(address, imapServer.Username, imapServer.Password, imapServer.Host, imapServer.Port)
+ })
+}
+
+// EML export.
+
+func userExportsToEMLFiles(bddUserID string) error {
+ return userExportsAddressToEMLFilesWithRules(bddUserID, "", nil)
+}
+
+func userExportsToEMLFilesWithRules(bddUserID string, rules *gherkin.DataTable) error {
+ return userExportsAddressToEMLFilesWithRules(bddUserID, "", rules)
+}
+
+func userExportsAddressToEMLFiles(bddUserID, bddAddressID string) error {
+ return userExportsAddressToEMLFilesWithRules(bddUserID, bddAddressID, nil)
+}
+
+func userExportsAddressToEMLFilesWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error {
+ return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) {
+ path := ctx.GetTransferLocalRootForExport()
+ return ctx.GetImportExport().GetEMLExporter(address, path)
+ })
+}
+
+// MBOX export.
+
+func userExportsToMBOXFiles(bddUserID string) error {
+ return userExportsAddressToMBOXFilesWithRules(bddUserID, "", nil)
+}
+
+func userExportsToMBOXFilesWithRules(bddUserID string, rules *gherkin.DataTable) error {
+ return userExportsAddressToMBOXFilesWithRules(bddUserID, "", rules)
+}
+
+func userExportsAddressToMBOXFiles(bddUserID, bddAddressID string) error {
+ return userExportsAddressToMBOXFilesWithRules(bddUserID, bddAddressID, nil)
+}
+
+func userExportsAddressToMBOXFilesWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error {
+ return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) {
+ path := ctx.GetTransferLocalRootForExport()
+ return ctx.GetImportExport().GetMBOXExporter(address, path)
+ })
+}
+
+// Helpers.
+
+func doTransfer(bddUserID, bddAddressID string, rules *gherkin.DataTable, getTransferrer func(string) (*transfer.Transfer, error)) error {
+ account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
+ if account == nil {
+ return godog.ErrPending
+ }
+ transferrer, err := getTransferrer(account.Address())
+ if err != nil {
+ return internalError(err, "failed to init transfer")
+ }
+ if err := setRules(transferrer, rules); err != nil {
+ return internalError(err, "failed to set rules")
+ }
+ progress := transferrer.Start()
+ ctx.SetTransferProgress(progress)
+ return nil
+}
+
+func setRules(transferrer *transfer.Transfer, rules *gherkin.DataTable) error {
+ if rules == nil {
+ return nil
+ }
+
+ transferrer.ResetRules()
+
+ allSourceMailboxes, err := transferrer.SourceMailboxes()
+ if err != nil {
+ return internalError(err, "failed to get source mailboxes")
+ }
+ allTargetMailboxes, err := transferrer.TargetMailboxes()
+ if err != nil {
+ return internalError(err, "failed to get target mailboxes")
+ }
+
+ head := rules.Rows[0].Cells
+ for _, row := range rules.Rows[1:] {
+ source := ""
+ target := ""
+ fromTime := int64(0)
+ toTime := int64(0)
+ for n, cell := range row.Cells {
+ switch head[n].Value {
+ case "source":
+ source = cell.Value
+ case "target":
+ target = cell.Value
+ case "from":
+ date, err := time.Parse(timeFormat, cell.Value)
+ if err != nil {
+ return internalError(err, "failed to parse from time")
+ }
+ fromTime = date.Unix()
+ case "to":
+ date, err := time.Parse(timeFormat, cell.Value)
+ if err != nil {
+ return internalError(err, "failed to parse to time")
+ }
+ toTime = date.Unix()
+ default:
+ return fmt.Errorf("unexpected column name: %s", head[n].Value)
+ }
+ }
+
+ sourceMailbox, err := getMailboxByName(allSourceMailboxes, source)
+ if err != nil {
+ return internalError(err, "failed to match source mailboxes")
+ }
+
+ // Empty target means the same as source. Useful for exports.
+ targetMailboxes := []transfer.Mailbox{}
+ if target == "" {
+ targetMailboxes = append(targetMailboxes, sourceMailbox)
+ } else {
+ targetMailbox, err := getMailboxByName(allTargetMailboxes, target)
+ if err != nil {
+ return internalError(err, "failed to match target mailboxes")
+ }
+ targetMailboxes = append(targetMailboxes, targetMailbox)
+ }
+
+ if err := transferrer.SetRule(sourceMailbox, targetMailboxes, fromTime, toTime); err != nil {
+ return internalError(err, "failed to set rule")
+ }
+ }
+ return nil
+}
+
+func getMailboxByName(mailboxes []transfer.Mailbox, name string) (transfer.Mailbox, error) {
+ for _, mailbox := range mailboxes {
+ if mailbox.Name == name {
+ return mailbox, nil
+ }
+ }
+ return transfer.Mailbox{}, fmt.Errorf("mailbox %s not found", name)
+}
diff --git a/test/transfer_checks_test.go b/test/transfer_checks_test.go
new file mode 100644
index 00000000..7452f8eb
--- /dev/null
+++ b/test/transfer_checks_test.go
@@ -0,0 +1,267 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package tests
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/mail"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/cucumber/godog"
+ "github.com/cucumber/godog/gherkin"
+ "github.com/emersion/go-mbox"
+ "github.com/emersion/go-message"
+ "github.com/pkg/errors"
+ a "github.com/stretchr/testify/assert"
+)
+
+func TransferChecksFeatureContext(s *godog.Suite) {
+ s.Step(`^progress result is "([^"]*)"$`, progressFinishedWith)
+ s.Step(`^transfer exported (\d+) messages$`, transferExportedNumberOfMessages)
+ s.Step(`^transfer imported (\d+) messages$`, transferImportedNumberOfMessages)
+ s.Step(`^transfer failed for (\d+) messages$`, transferFailedForNumberOfMessages)
+ s.Step(`^transfer exported messages$`, transferExportedMessages)
+ s.Step(`^exported messages match the original ones$`, exportedMessagesMatchTheOriginalOnes)
+}
+
+func progressFinishedWith(wantResponse string) error {
+ progress := ctx.GetTransferProgress()
+ // Wait till transport is finished.
+ for range progress.GetUpdateChannel() {
+ }
+
+ err := progress.GetFatalError()
+ if wantResponse == "OK" {
+ a.NoError(ctx.GetTestingT(), err)
+ } else {
+ a.EqualError(ctx.GetTestingT(), err, wantResponse)
+ }
+ return ctx.GetTestingError()
+}
+
+func transferExportedNumberOfMessages(wantCount int) error {
+ progress := ctx.GetTransferProgress()
+ _, _, exported, _, _ := progress.GetCounts() //nolint[dogsled]
+ a.Equal(ctx.GetTestingT(), uint(wantCount), exported)
+ return ctx.GetTestingError()
+}
+
+func transferImportedNumberOfMessages(wantCount int) error {
+ progress := ctx.GetTransferProgress()
+ _, imported, _, _, _ := progress.GetCounts() //nolint[dogsled]
+ a.Equal(ctx.GetTestingT(), uint(wantCount), imported)
+ return ctx.GetTestingError()
+}
+
+func transferFailedForNumberOfMessages(wantCount int) error {
+ progress := ctx.GetTransferProgress()
+ failedMessages := progress.GetFailedMessages()
+ a.Equal(ctx.GetTestingT(), wantCount, len(failedMessages), "failed messages: %v", failedMessages)
+ return ctx.GetTestingError()
+}
+
+func transferExportedMessages(messages *gherkin.DataTable) error {
+ expectedMessages := map[string][]MessageAttributes{}
+
+ head := messages.Rows[0].Cells
+ for _, row := range messages.Rows[1:] {
+ folder := ""
+ msg := MessageAttributes{}
+
+ for n, cell := range row.Cells {
+ switch head[n].Value {
+ case "folder":
+ folder = cell.Value
+ case "subject":
+ msg.subject = cell.Value
+ case "from":
+ msg.from = cell.Value
+ case "to":
+ msg.to = []string{cell.Value}
+ case "time":
+ date, err := time.Parse(timeFormat, cell.Value)
+ if err != nil {
+ return internalError(err, "failed to parse time")
+ }
+ msg.date = date.Unix()
+ default:
+ return fmt.Errorf("unexpected column name: %s", head[n].Value)
+ }
+ }
+
+ expectedMessages[folder] = append(expectedMessages[folder], msg)
+ sort.Sort(BySubject(expectedMessages[folder]))
+ }
+
+ exportRoot := ctx.GetTransferLocalRootForExport()
+ exportedMessages, err := readMessages(exportRoot)
+ if err != nil {
+ return errors.Wrap(err, "scanning exported messages")
+ }
+
+ a.Equal(ctx.GetTestingT(), expectedMessages, exportedMessages)
+ return ctx.GetTestingError()
+}
+
+func exportedMessagesMatchTheOriginalOnes() error {
+ importRoot := ctx.GetTransferLocalRootForImport()
+ exportRoot := ctx.GetTransferLocalRootForExport()
+
+ importMessages, err := readMessages(importRoot)
+ if err != nil {
+ return errors.Wrap(err, "scanning messages for import")
+ }
+ exportMessages, err := readMessages(exportRoot)
+ if err != nil {
+ return errors.Wrap(err, "scanning exported messages")
+ }
+ delete(exportMessages, "All Mail") // Ignore All Mail.
+
+ a.Equal(ctx.GetTestingT(), importMessages, exportMessages)
+ return ctx.GetTestingError()
+}
+
+func readMessages(root string) (map[string][]MessageAttributes, error) {
+ files, err := ioutil.ReadDir(root)
+ if err != nil {
+ return nil, err
+ }
+
+ messagesPerLabel := map[string][]MessageAttributes{}
+ for _, file := range files {
+ if !file.IsDir() {
+ fileReader, err := os.Open(filepath.Join(root, file.Name()))
+ if err != nil {
+ return nil, errors.Wrap(err, "opening file")
+ }
+
+ if filepath.Ext(file.Name()) == ".eml" {
+ label := filepath.Base(root)
+ msg, err := readMessageAttributes(fileReader)
+ if err != nil {
+ return nil, err
+ }
+ messagesPerLabel[label] = append(messagesPerLabel[label], msg)
+ sort.Sort(BySubject(messagesPerLabel[label]))
+ } else if filepath.Ext(file.Name()) == ".mbox" {
+ label := strings.TrimSuffix(file.Name(), ".mbox")
+ mboxReader := mbox.NewReader(fileReader)
+ for {
+ msgReader, err := mboxReader.NextMessage()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ return nil, errors.Wrap(err, "reading next message")
+ }
+ msg, err := readMessageAttributes(msgReader)
+ if err != nil {
+ return nil, err
+ }
+ messagesPerLabel[label] = append(messagesPerLabel[label], msg)
+ }
+ sort.Sort(BySubject(messagesPerLabel[label]))
+ }
+ } else {
+ subfolderRoot := filepath.Join(root, file.Name())
+ subfolderMessagesPerLabel, err := readMessages(subfolderRoot)
+ if err != nil {
+ return nil, err
+ }
+ for key, value := range subfolderMessagesPerLabel {
+ messagesPerLabel[key] = append(messagesPerLabel[key], value...)
+ sort.Sort(BySubject(messagesPerLabel[key]))
+ }
+ }
+ }
+ return messagesPerLabel, nil
+}
+
+type MessageAttributes struct {
+ subject string
+ from string
+ to []string
+ date int64
+}
+
+func readMessageAttributes(fileReader io.Reader) (MessageAttributes, error) {
+ entity, err := message.Read(fileReader)
+ if err != nil {
+ return MessageAttributes{}, errors.Wrap(err, "reading file")
+ }
+ date, err := parseTime(entity.Header.Get("date"))
+ if err != nil {
+ return MessageAttributes{}, errors.Wrap(err, "parsing date")
+ }
+ from, err := parseAddress(entity.Header.Get("from"))
+ if err != nil {
+ return MessageAttributes{}, errors.Wrap(err, "parsing from")
+ }
+ to, err := parseAddresses(entity.Header.Get("to"))
+ if err != nil {
+ return MessageAttributes{}, errors.Wrap(err, "parsing to")
+ }
+ return MessageAttributes{
+ subject: entity.Header.Get("subject"),
+ from: from,
+ to: to,
+ date: date.Unix(),
+ }, nil
+}
+
+func parseTime(input string) (time.Time, error) {
+ for _, format := range []string{time.RFC1123, time.RFC1123Z} {
+ t, err := time.Parse(format, input)
+ if err == nil {
+ return t, nil
+ }
+ }
+ return time.Time{}, errors.New("Unrecognized time format")
+}
+
+func parseAddresses(input string) ([]string, error) {
+ addresses, err := mail.ParseAddressList(input)
+ if err != nil {
+ return nil, err
+ }
+ result := []string{}
+ for _, address := range addresses {
+ result = append(result, address.Address)
+ }
+ return result, nil
+}
+
+func parseAddress(input string) (string, error) {
+ address, err := mail.ParseAddress(input)
+ if err != nil {
+ return "", err
+ }
+ return address.Address, nil
+}
+
+// BySubject implements sort.Interface based on the subject field.
+type BySubject []MessageAttributes
+
+func (a BySubject) Len() int { return len(a) }
+func (a BySubject) Less(i, j int) bool { return a[i].subject < a[j].subject }
+func (a BySubject) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
diff --git a/test/transfer_setup_test.go b/test/transfer_setup_test.go
new file mode 100644
index 00000000..b52bec46
--- /dev/null
+++ b/test/transfer_setup_test.go
@@ -0,0 +1,261 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package tests
+
+import (
+ "bytes"
+ "fmt"
+ "net/textproto"
+ "os"
+ "path/filepath"
+ "strconv"
+ "time"
+
+ "github.com/ProtonMail/proton-bridge/pkg/message"
+ "github.com/cucumber/godog"
+ "github.com/cucumber/godog/gherkin"
+ "github.com/emersion/go-imap"
+ "github.com/emersion/go-mbox"
+)
+
+func TransferSetupFeatureContext(s *godog.Suite) {
+ s.Step(`^there are EML files$`, thereAreEMLFiles)
+ s.Step(`^there is EML file "([^"]*)"$`, thereIsEMLFile)
+ s.Step(`^there is MBOX file "([^"]*)" with messages$`, thereIsMBOXFileWithMessages)
+ s.Step(`^there is MBOX file "([^"]*)"$`, thereIsMBOXFile)
+ s.Step(`^there are IMAP mailboxes$`, thereAreIMAPMailboxes)
+ s.Step(`^there are IMAP messages$`, thereAreIMAPMessages)
+ s.Step(`^there is IMAP message in mailbox "([^"]*)" with seq (\d+), uid (\d+), time "([^"]*)" and subject "([^"]*)"$`, thereIsIMAPMessage)
+}
+
+func thereAreEMLFiles(messages *gherkin.DataTable) error {
+ head := messages.Rows[0].Cells
+ for _, row := range messages.Rows[1:] {
+ fileName := ""
+ for n, cell := range row.Cells {
+ switch head[n].Value {
+ case "file":
+ fileName = cell.Value
+ case "from", "to", "subject", "time", "body":
+ default:
+ return fmt.Errorf("unexpected column name: %s", head[n].Value)
+ }
+ }
+
+ body := getBodyFromDataRow(head, row)
+ if err := createFile(fileName, body); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func thereIsEMLFile(fileName string, message *gherkin.DocString) error {
+ return createFile(fileName, message.Content)
+}
+
+func thereIsMBOXFileWithMessages(fileName string, messages *gherkin.DataTable) error {
+ mboxBuffer := &bytes.Buffer{}
+ mboxWriter := mbox.NewWriter(mboxBuffer)
+
+ head := messages.Rows[0].Cells
+ for _, row := range messages.Rows[1:] {
+ from := ""
+ for n, cell := range row.Cells {
+ switch head[n].Value {
+ case "from":
+ from = cell.Value
+ case "to", "subject", "time", "body":
+ default:
+ return fmt.Errorf("unexpected column name: %s", head[n].Value)
+ }
+ }
+
+ body := getBodyFromDataRow(head, row)
+
+ messageWriter, err := mboxWriter.CreateMessage(from, time.Now())
+ if err != nil {
+ return err
+ }
+ _, err = messageWriter.Write([]byte(body))
+ if err != nil {
+ return err
+ }
+ }
+
+ return createFile(fileName, mboxBuffer.String())
+}
+
+func thereIsMBOXFile(fileName string, messages *gherkin.DocString) error {
+ return createFile(fileName, messages.Content)
+}
+
+func thereAreIMAPMailboxes(mailboxes *gherkin.DataTable) error {
+ imapServer := ctx.GetTransferRemoteIMAPServer()
+ head := mailboxes.Rows[0].Cells
+ for _, row := range mailboxes.Rows[1:] {
+ mailboxName := ""
+ for n, cell := range row.Cells {
+ switch head[n].Value {
+ case "name":
+ mailboxName = cell.Value
+ default:
+ return fmt.Errorf("unexpected column name: %s", head[n].Value)
+ }
+ }
+ imapServer.AddMailbox(mailboxName)
+ }
+ return nil
+}
+
+func thereAreIMAPMessages(messages *gherkin.DataTable) (err error) {
+ imapServer := ctx.GetTransferRemoteIMAPServer()
+ head := messages.Rows[0].Cells
+ for _, row := range messages.Rows[1:] {
+ mailboxName := ""
+ date := time.Now()
+ subject := ""
+ seqNum := 0
+ uid := 0
+ for n, cell := range row.Cells {
+ switch head[n].Value {
+ case "mailbox":
+ mailboxName = cell.Value
+ case "uid":
+ uid, err = strconv.Atoi(cell.Value)
+ if err != nil {
+ return internalError(err, "failed to parse uid")
+ }
+ case "seqnum":
+ seqNum, err = strconv.Atoi(cell.Value)
+ if err != nil {
+ return internalError(err, "failed to parse seqnum")
+ }
+ case "time":
+ date, err = time.Parse(timeFormat, cell.Value)
+ if err != nil {
+ return internalError(err, "failed to parse time")
+ }
+ case "subject":
+ subject = cell.Value
+ case "from", "to", "body":
+ default:
+ return fmt.Errorf("unexpected column name: %s", head[n].Value)
+ }
+ }
+
+ body := getBodyFromDataRow(head, row)
+ imapMessage, err := getIMAPMessage(seqNum, uid, date, subject, body)
+ if err != nil {
+ return err
+ }
+ imapServer.AddMessage(mailboxName, imapMessage)
+ }
+ return nil
+}
+
+func thereIsIMAPMessage(mailboxName string, seqNum, uid int, dateValue, subject string, message *gherkin.DocString) error {
+ imapServer := ctx.GetTransferRemoteIMAPServer()
+
+ date, err := time.Parse(timeFormat, dateValue)
+ if err != nil {
+ return internalError(err, "failed to parse time")
+ }
+
+ imapMessage, err := getIMAPMessage(seqNum, uid, date, subject, message.Content)
+ if err != nil {
+ return err
+ }
+ imapServer.AddMessage(mailboxName, imapMessage)
+
+ return nil
+}
+
+func getBodyFromDataRow(head []*gherkin.TableCell, row *gherkin.TableRow) string {
+ body := "hello"
+ headers := textproto.MIMEHeader{}
+ for n, cell := range row.Cells {
+ switch head[n].Value {
+ case "from":
+ headers.Set("from", cell.Value)
+ case "to":
+ headers.Set("to", cell.Value)
+ case "subject":
+ headers.Set("subject", cell.Value)
+ case "time":
+ date, err := time.Parse(timeFormat, cell.Value)
+ if err != nil {
+ panic(err)
+ }
+ headers.Set("date", date.Format(time.RFC1123))
+ case "body":
+ body = cell.Value
+ }
+ }
+
+ buffer := &bytes.Buffer{}
+ _ = message.WriteHeader(buffer, headers)
+ return buffer.String() + body + "\n\n"
+}
+
+func getIMAPMessage(seqNum, uid int, date time.Time, subject, body string) (*imap.Message, error) {
+ reader := bytes.NewBufferString(body)
+ bodyStructure, err := message.NewBodyStructure(reader)
+ if err != nil {
+ return nil, internalError(err, "failed to parse body structure")
+ }
+ imapBodyStructure, err := bodyStructure.IMAPBodyStructure([]int{})
+ if err != nil {
+ return nil, internalError(err, "failed to parse body structure")
+ }
+ bodySection, _ := imap.ParseBodySectionName("BODY[]")
+
+ return &imap.Message{
+ SeqNum: uint32(seqNum),
+ Uid: uint32(uid),
+ Size: uint32(len(body)),
+ Envelope: &imap.Envelope{
+ Date: date,
+ Subject: subject,
+ },
+ BodyStructure: imapBodyStructure,
+ Body: map[*imap.BodySectionName]imap.Literal{
+ bodySection: bytes.NewBufferString(body),
+ },
+ }, nil
+}
+
+func createFile(fileName, body string) error {
+ root := ctx.GetTransferLocalRootForImport()
+ filePath := filepath.Join(root, fileName)
+
+ dirPath := filepath.Dir(filePath)
+ err := os.MkdirAll(dirPath, os.ModePerm)
+ if err != nil {
+ return internalError(err, "failed to create dir")
+ }
+
+ f, err := os.Create(filePath)
+ if err != nil {
+ return internalError(err, "failed to create file")
+ }
+ defer f.Close() //nolint
+
+ _, err = f.WriteString(body)
+ return internalError(err, "failed to write to file")
+}
diff --git a/test/users_actions_test.go b/test/users_actions_test.go
new file mode 100644
index 00000000..98fa229c
--- /dev/null
+++ b/test/users_actions_test.go
@@ -0,0 +1,125 @@
+// Copyright (c) 2020 Proton Technologies AG
+//
+// This file is part of ProtonMail Bridge.Bridge.
+//
+// ProtonMail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ProtonMail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ProtonMail Bridge. If not, see .
+
+package tests
+
+import (
+ "github.com/cucumber/godog"
+)
+
+func UsersActionsFeatureContext(s *godog.Suite) {
+ s.Step(`^"([^"]*)" logs in$`, userLogsIn)
+ s.Step(`^"([^"]*)" logs in with bad password$`, userLogsInWithBadPassword)
+ s.Step(`^"([^"]*)" logs out$`, userLogsOut)
+ s.Step(`^"([^"]*)" changes the address mode$`, userChangesTheAddressMode)
+ s.Step(`^user deletes "([^"]*)"$`, userDeletesUser)
+ s.Step(`^user deletes "([^"]*)" with cache$`, userDeletesUserWithCache)
+ s.Step(`^"([^"]*)" swaps address "([^"]*)" with address "([^"]*)"$`, swapsAddressWithAddress)
+}
+
+func userLogsIn(bddUserID string) error {
+ account := ctx.GetTestAccount(bddUserID)
+ if account == nil {
+ return godog.ErrPending
+ }
+ ctx.SetLastError(ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()))
+ return nil
+}
+
+func userLogsInWithBadPassword(bddUserID string) error {
+ account := ctx.GetTestAccount(bddUserID)
+ if account == nil {
+ return godog.ErrPending
+ }
+ ctx.SetLastError(ctx.LoginUser(account.Username(), "you shall not pass!", "123"))
+ return nil
+}
+
+func userLogsOut(bddUserID string) error {
+ account := ctx.GetTestAccount(bddUserID)
+ if account == nil {
+ return godog.ErrPending
+ }
+ ctx.SetLastError(ctx.LogoutUser(account.Username()))
+ return nil
+}
+
+func userChangesTheAddressMode(bddUserID string) error {
+ account := ctx.GetTestAccount(bddUserID)
+ if account == nil {
+ return godog.ErrPending
+ }
+ user, err := ctx.GetUser(account.Username())
+ if err != nil {
+ return internalError(err, "getting user %s", account.Username())
+ }
+ if err := user.SwitchAddressMode(); err != nil {
+ return err
+ }
+
+ ctx.EventuallySyncIsFinishedForUsername(account.Username())
+ return nil
+}
+
+func userDeletesUser(bddUserID string) error {
+ return deleteUser(bddUserID, false)
+}
+
+func userDeletesUserWithCache(bddUserID string) error {
+ return deleteUser(bddUserID, true)
+}
+
+func deleteUser(bddUserID string, cache bool) error {
+ account := ctx.GetTestAccount(bddUserID)
+ if account == nil {
+ return godog.ErrPending
+ }
+ user, err := ctx.GetUser(account.Username())
+ if err != nil {
+ return internalError(err, "getting user %s", account.Username())
+ }
+ ctx.SetLastError(ctx.GetUsers().DeleteUser(user.ID(), cache))
+ return nil
+}
+
+func swapsAddressWithAddress(bddUserID, bddAddressID1, bddAddressID2 string) error {
+ account := ctx.GetTestAccount(bddUserID)
+ if account == nil {
+ return godog.ErrPending
+ }
+
+ address1ID := account.GetAddressID(bddAddressID1)
+ address2ID := account.GetAddressID(bddAddressID2)
+ addressIDs := make([]string, len(*account.Addresses()))
+
+ var address1Index, address2Index int
+ for i, v := range *account.Addresses() {
+ if v.ID == address1ID {
+ address1Index = i
+ }
+ if v.ID == address2ID {
+ address2Index = i
+ }
+ addressIDs[i] = v.ID
+ }
+
+ addressIDs[address1Index], addressIDs[address2Index] = addressIDs[address2Index], addressIDs[address1Index]
+
+ ctx.ReorderAddresses(account.Username(), bddAddressID1, bddAddressID2)
+
+ return ctx.GetPMAPIController().ReorderAddresses(account.User(), addressIDs)
+}
diff --git a/test/bridge_checks_test.go b/test/users_checks_test.go
similarity index 77%
rename from test/bridge_checks_test.go
rename to test/users_checks_test.go
index 980fecba..b816c1dc 100644
--- a/test/bridge_checks_test.go
+++ b/test/users_checks_test.go
@@ -24,8 +24,7 @@ import (
a "github.com/stretchr/testify/assert"
)
-func BridgeChecksFeatureContext(s *godog.Suite) {
- s.Step(`^bridge response is "([^"]*)"$`, bridgeResponseIs)
+func UsersChecksFeatureContext(s *godog.Suite) {
s.Step(`^"([^"]*)" has address mode in "([^"]*)" mode$`, userHasAddressModeInMode)
s.Step(`^"([^"]*)" is disconnected$`, userIsDisconnected)
s.Step(`^"([^"]*)" is connected$`, userIsConnected)
@@ -39,27 +38,17 @@ func BridgeChecksFeatureContext(s *godog.Suite) {
s.Step(`^"([^"]*)" has API auth$`, isAuthorized)
}
-func bridgeResponseIs(expectedResponse string) error {
- err := ctx.GetLastBridgeError()
- if expectedResponse == "OK" {
- a.NoError(ctx.GetTestingT(), err)
- } else {
- a.EqualError(ctx.GetTestingT(), err, expectedResponse)
- }
- return ctx.GetTestingError()
-}
-
func userHasAddressModeInMode(bddUserID, wantAddressMode string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
- bridgeUser, err := ctx.GetUser(account.Username())
+ user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
addressMode := "split"
- if bridgeUser.IsCombinedAddressMode() {
+ if user.IsCombinedAddressMode() {
addressMode = "combined"
}
a.Equal(ctx.GetTestingT(), wantAddressMode, addressMode)
@@ -71,12 +60,12 @@ func userIsDisconnected(bddUserID string) error {
if account == nil {
return godog.ErrPending
}
- bridgeUser, err := ctx.GetUser(account.Username())
+ user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
a.Eventually(ctx.GetTestingT(), func() bool {
- return !bridgeUser.IsConnected()
+ return !user.IsConnected()
}, 5*time.Second, 10*time.Millisecond)
return ctx.GetTestingError()
}
@@ -87,13 +76,13 @@ func userIsConnected(bddUserID string) error {
return godog.ErrPending
}
t := ctx.GetTestingT()
- bridgeUser, err := ctx.GetUser(account.Username())
+ user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
- a.Eventually(ctx.GetTestingT(), bridgeUser.IsConnected, 5*time.Second, 10*time.Millisecond)
- a.NotEmpty(t, bridgeUser.GetPrimaryAddress())
- a.NotEmpty(t, bridgeUser.GetStoreAddresses())
+ a.Eventually(ctx.GetTestingT(), user.IsConnected, 5*time.Second, 10*time.Millisecond)
+ a.NotEmpty(t, user.GetPrimaryAddress())
+ a.NotEmpty(t, user.GetStoreAddresses())
return ctx.GetTestingError()
}
@@ -122,11 +111,11 @@ func userHasLoadedStore(bddUserID string) error {
if account == nil {
return godog.ErrPending
}
- bridgeUser, err := ctx.GetUser(account.Username())
+ user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
- a.NotNil(ctx.GetTestingT(), bridgeUser.GetStore())
+ a.NotNil(ctx.GetTestingT(), user.GetStore())
return ctx.GetTestingError()
}
@@ -135,11 +124,11 @@ func userDoesNotHaveLoadedStore(bddUserID string) error {
if account == nil {
return godog.ErrPending
}
- bridgeUser, err := ctx.GetUser(account.Username())
+ user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
- a.Nil(ctx.GetTestingT(), bridgeUser.GetStore())
+ a.Nil(ctx.GetTestingT(), user.GetStore())
return ctx.GetTestingError()
}
@@ -173,28 +162,28 @@ func userDoesNotHaveRunningEventLoop(bddUserID string) error {
return ctx.GetTestingError()
}
-func isAuthorized(accountName string) error {
- account := ctx.GetTestAccount(accountName)
+func isAuthorized(bddUserID string) error {
+ account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
- bridgeUser, err := ctx.GetUser(account.Username())
+ user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
- a.Eventually(ctx.GetTestingT(), bridgeUser.IsAuthorized, 5*time.Second, 10*time.Millisecond)
+ a.Eventually(ctx.GetTestingT(), user.IsAuthorized, 5*time.Second, 10*time.Millisecond)
return ctx.GetTestingError()
}
-func isNotAuthorized(accountName string) error {
- account := ctx.GetTestAccount(accountName)
+func isNotAuthorized(bddUserID string) error {
+ account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
- bridgeUser, err := ctx.GetUser(account.Username())
+ user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
- a.Eventually(ctx.GetTestingT(), func() bool { return !bridgeUser.IsAuthorized() }, 5*time.Second, 10*time.Millisecond)
+ a.Eventually(ctx.GetTestingT(), func() bool { return !user.IsAuthorized() }, 5*time.Second, 10*time.Millisecond)
return ctx.GetTestingError()
}
diff --git a/test/bridge_setup_test.go b/test/users_setup_test.go
similarity index 89%
rename from test/bridge_setup_test.go
rename to test/users_setup_test.go
index 261d27a8..495e574c 100644
--- a/test/bridge_setup_test.go
+++ b/test/users_setup_test.go
@@ -25,8 +25,7 @@ import (
a "github.com/stretchr/testify/assert"
)
-func BridgeSetupFeatureContext(s *godog.Suite) {
- s.Step(`^there is no internet connection$`, thereIsNoInternetConnection)
+func UsersSetupFeatureContext(s *godog.Suite) {
s.Step(`^there is user "([^"]*)"$`, thereIsUser)
s.Step(`^there is connected user "([^"]*)"$`, thereIsConnectedUser)
s.Step(`^there is disconnected user "([^"]*)"$`, thereIsDisconnectedUser)
@@ -35,11 +34,6 @@ func BridgeSetupFeatureContext(s *godog.Suite) {
s.Step(`^there is "([^"]*)" in "([^"]*)" address mode$`, thereIsUserWithAddressMode)
}
-func thereIsNoInternetConnection() error {
- ctx.GetPMAPIController().TurnInternetConnectionOff()
- return nil
-}
-
func thereIsUser(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
@@ -87,7 +81,11 @@ func thereIsDisconnectedUser(bddUserID string) error {
// logout is also called and if we would do login at the same time, it
// wouldn't work. 100 ms after event loop is stopped should be enough.
a.Eventually(ctx.GetTestingT(), func() bool {
- return !user.GetStore().TestGetEventLoop().IsRunning()
+ store := user.GetStore()
+ if store == nil {
+ return true
+ }
+ return !store.TestGetEventLoop().IsRunning()
}, 1*time.Second, 10*time.Millisecond)
time.Sleep(100 * time.Millisecond)
return ctx.GetTestingError()
@@ -120,20 +118,20 @@ func thereIsUserWithAddressMode(bddUserID, wantAddressMode string) error {
if account == nil {
return godog.ErrPending
}
- bridgeUser, err := ctx.GetUser(account.Username())
+ user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
addressMode := "split"
- if bridgeUser.IsCombinedAddressMode() {
+ if user.IsCombinedAddressMode() {
addressMode = "combined"
}
if wantAddressMode != addressMode {
- err := bridgeUser.SwitchAddressMode()
+ err := user.SwitchAddressMode()
if err != nil {
return internalError(err, "switching mode")
}
}
- ctx.EventuallySyncIsFinishedForUsername(bridgeUser.Username())
+ ctx.EventuallySyncIsFinishedForUsername(user.Username())
return nil
}