mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Import/Export backend
This commit is contained in:
@ -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:
|
||||
|
||||
7
Makefile
7
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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
go.mod
1
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
|
||||
|
||||
@ -15,16 +15,16 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package 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_") {
|
||||
86
internal/cmd/main.go
Normal file
86
internal/cmd/main.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
43
internal/cmd/memory_profile.go
Normal file
43
internal/cmd/memory_profile.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
||||
}
|
||||
108
internal/cmd/restart.go
Normal file
108
internal/cmd/restart.go
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package 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)
|
||||
}
|
||||
32
internal/cmd/version_file.go
Normal file
32
internal/cmd/version_file.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package cli 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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at 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;"
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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{}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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{}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = ""
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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}, "/")
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
45
test/api_actions_test.go
Normal file
45
test/api_actions_test.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
31
test/api_setup_test.go
Normal file
31
test/api_setup_test.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
func benchTestContext() (*context.TestContext, *mocks.IMAPClient) {
|
||||
ctx := context.New()
|
||||
ctx := context.New("bridge")
|
||||
|
||||
username := "user"
|
||||
account := ctx.GetTestAccount(username)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
37
test/common_checks_test.go
Normal file
37
test/common_checks_test.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
48
test/context/importexport.go
Normal file
48
test/context/importexport.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
@ -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
|
||||
|
||||
91
test/context/transfer.go
Normal file
91
test/context/transfer.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
62
test/fakeapi/keyring_userKey
Normal file
62
test/fakeapi/keyring_userKey
Normal file
@ -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-----
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
@ -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 |
|
||||
@ -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"
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 |
|
||||
43
test/features/ie/transfer/export_eml.feature
Normal file
43
test/features/ie/transfer/export_eml.feature
Normal file
@ -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 |
|
||||
43
test/features/ie/transfer/export_mbox.feature
Normal file
43
test/features/ie/transfer/export_mbox.feature
Normal file
@ -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 |
|
||||
60
test/features/ie/transfer/import_eml.feature
Normal file
60
test/features/ie/transfer/import_eml.feature
Normal file
@ -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 <bridgetest@pm.test>
|
||||
To: Internal Bridge <test@protonmail.com>
|
||||
|
||||
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
|
||||
49
test/features/ie/transfer/import_export.feature
Normal file
49
test/features/ie/transfer/import_export.feature
Normal file
@ -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
|
||||
79
test/features/ie/transfer/import_imap.feature
Normal file
79
test/features/ie/transfer/import_imap.feature
Normal file
@ -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 <bridgetest@pm.test>
|
||||
To: Internal Bridge <test@protonmail.com>
|
||||
|
||||
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
|
||||
64
test/features/ie/transfer/import_mbox.feature
Normal file
64
test/features/ie/transfer/import_mbox.feature
Normal file
@ -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 <bridgetest@pm.test>
|
||||
To: Internal Bridge <test@protonmail.com>
|
||||
|
||||
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
|
||||
20
test/features/ie/users/delete.feature
Normal file
20
test/features/ie/users/delete.feature
Normal file
@ -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"
|
||||
56
test/features/ie/users/login.feature
Normal file
56
test/features/ie/users/login.feature
Normal file
@ -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
|
||||
12
test/features/ie/users/relogin.feature
Normal file
12
test/features/ie/users/relogin.feature
Normal file
@ -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
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
227
test/mocks/imap_server.go
Normal file
227
test/mocks/imap_server.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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")
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user