Compare commits

..

66 Commits

Author SHA1 Message Date
bfdfc81d65 release notes 2020-09-07 08:03:02 +02:00
bf6963859f rename IE app GODT-690 2020-09-04 11:55:30 +02:00
bc07896436 Sentry report after parser panic 2020-08-31 17:23:22 +02:00
1d2e584799 Convert panics from message parser to error 2020-08-31 15:57:45 +02:00
61867fbde7 Add hour when days don't match GODT-655 2020-08-24 10:11:51 +02:00
2d9417d501 Migrate from old credentials 2020-08-24 10:11:51 +02:00
4973e38748 Import-Export app everywhere 2020-08-24 10:11:51 +02:00
e4704cd459 Release notes 2020-08-24 10:11:51 +02:00
8592a264c0 Fix showing error msg 2020-08-24 10:11:51 +02:00
40aeb6c010 Fixing IE icon 2020-08-24 10:11:51 +02:00
71b9a3b205 Release notes for Import-Export 2020-08-24 10:11:51 +02:00
2182e573f9 Update maximal date on every DateInput dropdown toggle 2020-08-24 10:11:51 +02:00
5f02e59fa4 Fix showing table with errors 2020-08-24 10:11:51 +02:00
29ff8cf54b fix: double colon in window title again 2020-08-24 10:11:51 +02:00
f8cf4e966f fix: double colon in window title 2020-08-24 10:11:51 +02:00
df80e7eb27 Keep Import-Export credits up to date 2020-08-24 10:11:51 +02:00
658ead9fb3 Import/Export final touches 2020-08-24 10:11:51 +02:00
4f0af0fb02 Import/Export metrics 2020-08-24 10:11:51 +02:00
7e5e3d3dd4 Import/Export GUI 2020-08-24 10:11:51 +02:00
1c10cc5065 Import/Export backend 2020-08-24 10:11:51 +02:00
49316a935c Shared GUI for Bridge and Import/Export 2020-08-24 10:11:50 +02:00
b598779c0f Import/Export backend prep 2020-08-24 10:11:50 +02:00
9d65192ad7 feat: clear expired cookies from persistent storage 2020-08-24 09:08:42 +02:00
0e14155185 fix: cookie expiry needs to be set 2020-08-21 12:42:49 +02:00
56f4f3d017 fix: better error handling when message is still in send queue 2020-08-19 15:25:38 +02:00
f5617ced3f fix (GODT-597): duplicate send when draft creation takes a long time 2020-08-18 13:40:56 +00:00
35b37c7097 fix (GODT-597): duplicate send when draft creation takes a long time 2020-08-18 13:40:56 +00:00
77c6ba381e fix: mime type 2020-08-18 09:14:46 +00:00
34df24ede3 docs: changelog 2020-08-18 09:14:46 +00:00
33d705a39d test: fix test content types 2020-08-18 09:14:46 +00:00
64fbd0655f fix: sign when no contact present 2020-08-18 09:14:46 +00:00
b700a7823e fix: send to internal 2020-08-18 09:14:46 +00:00
145da7ffa5 refactor: pass mailSettings in to avoid extra call 2020-08-18 09:14:46 +00:00
61a841ced7 refactor: builder pattern for generateSendingInfo 2020-08-18 09:14:46 +00:00
29978b7014 ci: use bridge-internal image 2020-08-17 08:28:32 +00:00
dd73687555 ci: switch to using container from bridge-internal 2020-08-17 08:28:32 +00:00
5411b29d17 Merge branch 'release/v1.3.X' into devel 2020-08-17 09:58:39 +02:00
6c93f1f1ec Fix integration tests - compiting message flags 2020-08-17 09:10:03 +02:00
1dcaa200e0 fix: docstring mistakes 2020-08-13 11:54:52 +02:00
66082af40f test: add test that cookie jar loads cookies 2020-08-13 11:39:28 +02:00
209af59232 refactor: make cookie architecture less crazy 2020-08-13 11:31:11 +02:00
9f24c666b9 docs: add docstrings 2020-08-12 15:46:19 +02:00
3101fc5543 fix: add missing license 2020-08-12 15:05:14 +02:00
182bbd556f fix: failure to create cookie jar is not fatal error 2020-08-12 14:56:55 +02:00
e333ccd29e feat: persistent cookies 2020-08-12 14:55:24 +02:00
ce4a75caf5 fix: properly decide whether it is first gui start 2020-08-06 09:29:30 +02:00
34ef9063cb fix: better first start setting 2020-08-05 15:20:20 +02:00
f651d39820 chore: bump dependencies 2020-08-03 07:47:49 +00:00
7e6d09a247 test: generate tls cert/key in test 2020-08-03 09:24:39 +02:00
da381130a3 Check log file size more often to prevent huge log files 2020-07-31 13:24:25 +02:00
be07cb83c9 chore: bump linter to v1.29.0 2020-07-28 08:37:40 +00:00
dfbd86c7bc fix: add missing option to modify system keychain 2020-07-27 17:23:59 +02:00
e3ab829ad3 fix: missing command in exec call 2020-07-27 13:05:46 +02:00
b12ef1327c refactor: better confirmer result locking 2020-07-24 13:04:29 +00:00
d66bcc4b63 fix: bad ID in frontend 2020-07-24 13:04:29 +00:00
5ad307868e feat: add expiry 2020-07-24 13:04:29 +00:00
369c6ebf85 fix: clean up after setting result 2020-07-24 13:04:29 +00:00
c988d739a1 docs: add docstrings for confirmer 2020-07-24 13:04:29 +00:00
36ef9f20ae feat: use confirmer in smtp 2020-07-24 13:04:29 +00:00
c8f118a26b feat: implement confirmer 2020-07-24 13:04:29 +00:00
be20714842 feat: better way to add trusted cert in macOS 2020-07-24 14:51:30 +02:00
1711442878 Fix setting flags 2020-07-23 14:49:43 +02:00
79e6799f40 fix: panic in integration tests 2020-07-22 11:04:23 +00:00
6023162443 feat: add build tag for deterministic imap password 2020-07-22 09:32:20 +00:00
01e0fe4863 ci: use shared macos runner 2020-07-21 16:58:47 +02:00
1df81e4a34 chore: bump changelog 2020-07-16 11:51:56 +02:00
320 changed files with 24620 additions and 2194 deletions

30
.gitignore vendored
View File

@ -18,23 +18,29 @@ coverage.html
mem.pprof
# Auto generated frontend
frontend/qml/BridgeUI/*.qmlc
frontend/qml/ProtonUI/*.qmlc
frontend/qml/ProtonUI/fontawesome.ttf
frontend/qml/ProtonUI/images
internal/frontend/qml/BridgeUI/*.qmlc
internal/frontend/qml/ImportExportUI/*.qmlc
internal/frontend/qml/ProtonUI/*.qmlc
internal/frontend/qml/ProtonUI/fontawesome.ttf
internal/frontend/qml/ProtonUI/images
internal/frontend/qml/ImportExportUI/images
frontend/qml/*.qmlc
# Build files
bridge_darwin_*.tgz
cmd/Desktop-Bridge/deploy
internal/frontend/qt/moc.cpp
internal/frontend/qt/moc.go
internal/frontend/qt/moc.h
internal/frontend/qt/moc_cgo_darwin_darwin_amd64.go
internal/frontend/qt/moc_moc.h
internal/frontend/qt/rcc.cpp
internal/frontend/qt/rcc_cgo_darwin_darwin_amd64.go
internal/frontend/qt*/moc.cpp
internal/frontend/qt*/moc.go
internal/frontend/qt*/moc.h
internal/frontend/qt*/moc_cgo_*.go
internal/frontend/qt*/moc_moc.h
internal/frontend/qt*/rcc.cpp
internal/frontend/qt*/rcc.qrc
internal/frontend/qt*/rcc_cgo_*.go
internal/frontend/rcc.cpp
internal/frontend/rcc.qrc
internal/frontend/rcc_cgo_darwin_darwin_amd64.go
internal/frontend/rcc_cgo_*.go
vendor-cache/
/main.go

View File

@ -1,4 +1,4 @@
image: gitlab.protontech.ch:4567/go/bridge/ci
image: gitlab.protontech.ch:4567/go/bridge-internal
before_script:
- eval $(ssh-agent -s)
@ -13,38 +13,15 @@ before_script:
cache:
key: go-mod
paths:
- .cache
- .cache
policy: pull
stages:
- image
- cache
- test
- build
- mirror
# Stage: IMAGE
build-ci-image:
stage: image
image: docker:stable
before_script: []
cache: {}
tags:
- heavy
only:
changes:
- ci/*
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker info
- docker build -t gitlab.protontech.ch:4567/go/bridge/ci:latest ci
- docker push gitlab.protontech.ch:4567/go/bridge/ci:latest
# Stage: CACHE
# This will ensure latest dependency versions and updates the cache for
@ -54,11 +31,11 @@ cache-push:
only:
- branches
script:
- echo ""
- echo ""
cache:
key: go-mod
paths:
- .cache
- .cache
# Stage: TEST
@ -98,23 +75,33 @@ dependency-updates:
# Stage: BUILD
build-linux:
.build-base:
stage: build
# Test build every time (= we want to know build is possible).
only:
- branches
script:
- make build
artifacts:
expire_in: 2 week
build-linux:
extends: .build-base
artifacts:
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
expire_in: 2 week
build-darwin:
stage: build
only:
- branches
build-ie-linux:
extends: .build-base
script:
- make build-ie
artifacts:
name: "ie-linux-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
.build-darwin-base:
extends: .build-base
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
@ -125,25 +112,36 @@ build-darwin:
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOPATH=~/go
- export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
cache: {}
tags:
- macOS-bridge
script:
- make build
- macOS
build-darwin:
extends: .build-darwin-base
artifacts:
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
expire_in: 2 week
build-windows:
stage: build
build-ie-darwin:
extends: .build-darwin-base
script:
- make build-ie
artifacts:
name: "ie-darwin-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
.build-windows-base:
extends: .build-base
services:
- docker:dind
only:
- branches
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
build-windows:
extends: .build-windows-base
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.
@ -156,7 +154,23 @@ build-windows:
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
expire_in: 2 week
build-ie-windows:
extends: .build-windows-base
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: "ie-windows-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
# Stage: MIRROR
mirror-repo:
stage: mirror

View File

@ -21,6 +21,12 @@ issues:
- gochecknoinits
- gosec
linters-settings:
godox:
keywords:
- TODO
- FIXME
linters:
# setting disable-all will make only explicitly enabled linters run
disable-all: true

View File

@ -1,4 +1,4 @@
# Building ProtonMail Bridge app
# Building ProtonMail Bridge and Import-Export app
## Prerequisites
* Go 1.13
@ -19,6 +19,8 @@ Otherwise, the sending of crash reports will be disabled.
export MSYSTEM=
```
### Build Bridge
* in project root run
```bash
@ -26,9 +28,22 @@ make build
```
* The result will be stored in `./cmd/Destop-Bridge/deploy/${GOOS}/`
* for `linux`, the binary will have the name of the project directory (e.g `bridge`)
* for `windows`, the binary will have the file extension `.exe` (e.g `bridge.exe`)
* for `darwin`, the application will be created with name of the project directory (e.g `bridge.app`)
* for `linux`, the binary will have the name of the project directory (e.g `proton-bridge`)
* for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`)
* for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`)
### Build Import-Export
* in project root run
```bash
make build-ie
```
* The result will be stored in `./cmd/Import-Export/deploy/${GOOS}/`
* for `linux`, the binary will have the name of the project directory (e.g `proton-bridge`)
* for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`)
* for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`)
## Useful tests, lints and checks
In order to be able to run following commands please install the development dependencies:

View File

@ -4,7 +4,59 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Unreleased
## [v1.3.x] Emma (beta since 2020-08-05)
## [IE 0.2.x] Congo
### Added
* GODT-633 Persistent anonymous API cookies for better load balancing and abuse detection.
### Changed
* GODT-409 Set flags have to replace all flags.
* GODT-531 Better way to add trusted certificate in macOS.
* Bumped golangci-lint to v1.29.0
* GODT-549 Check log file size more often to prevent huge log files.
* Bumped various dependencies:
* andybalholm/cascadia v1.1.0 -> v1.2.0
* emersion/go-imap-specialuse 20161227184202-ba031ced6a62 -> 20200722111535-598ff00e4075
* emersion/go-sasl 20191210011802-430746ea8b9b -> 20200509203442-7bfe0ed36a21
* github.com/go-resty/resty/v2 v2.2.0 -> v2.3.0
* github.com/golang/mock v1.4.3 -> v1.4.4
* github.com/google/go-cmp v0.4.0 -> v0.5.1
* github.com/hashicorp/go-multierror v1.0.0 -> v1.1.0
* github.com/jaytaylor/html2text 20200220170450-61d9dc4d7195 -> 20200412013138-3577fbdbcff7
* github.com/jhillyerd/enmime v0.8.0 -> v0.8.1
* github.com/keybase/go-keychain 20200218013740-86d4642e4ce2 -> 20200502122510-cda31fe0c86d
* github.com/logrusorgru/aurora 20200102142835-e9ef32dff381 -> v2.0.3+incompatible
* github.com/miekg/dns v1.1.29 -> v1.1.30
* github.com/nsf/jsondiff 20190712045011-8443391ee9b6 -> 20200515183724-f29ed568f4ce
* github.com/sirupsen/logrus v1.4.2 -> v1.6.0
* github.com/stretchr/testify v1.5.1 -> v1.6.1
* github.com/therecipe/qt 20200126204426-5074eb6d8c41 -> 20200701200531-7f61353ee73e
* github.com/urfave/cli v1.22.3 -> v1.22.4
* golang.org/x/net 20200301022130-244492dfa37a -> 20200707034311-ab3426394381
* golang.org/x/text v0.3.2 -> v0.3.3
* Set first-start to false in bridge, not in frontend.
* GODT-400 Refactor sendingInfo.
* GODT-380 Adding IE GUI to Bridge repo and building
* BR: extend functionality of PopupDialog
* BR: makefile APP_VERSION instead of BRIDGE_VERSION
* BR: use common logs function for Qt
* BR: change `go.progressDescription` to `string`
* IE: Rounded button has fa-icon
* IE: `Upgrade``Update`
* IE: Moving `AccountModel` to `qt-common`
* IE: Added `ReportBug` to `internal/importexport`
* IE: Added event watch in GUI
* IE: Removed `onLoginFinished`
* Structure for transfer rules in QML
* GODT-213 Convert panics from message parser to error.
### Fixed
* GODT-655 Fix date picker with automatic Windows DST
* GODT-454 Fix send on closed channel when receiving unencrypted send confirmation from GUI.
* GODT-597 Duplicate sending when draft creation takes too long
## [v1.3.x] Emma (v1.3.2 beta 2020-08-04, v1.3.3 beta 2020-08-06, v1.3.3 live 2020-08-12)
### Added
* GODT-554 Detect and notify about "bad certificate" IMAP TLS error.

106
Makefile
View File

@ -3,19 +3,33 @@ export GO111MODULE=on
# By default, the target OS is the same as the host OS,
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
GOOS:=$(shell go env GOOS)
TARGET_CMD?=Desktop-Bridge
TARGET_OS?=${GOOS}
## Build
.PHONY: build build-nogui check-has-go
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
BRIDGE_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}
SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns
SRC_SVG:=logo.svg
TGT_ICNS:=Bridge.icns
ifeq "${TARGET_CMD}" "Import-Export"
APP_VERSION:=${IE_APP_VERSION}
SRC_ICO:=ie.ico
SRC_ICNS:=ie.icns
SRC_SVG:=ie.svg
TGT_ICNS:=ImportExport.icns
endif
REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z)
BUILD_TAGS?=pmapi_prod
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/pkg/constants.,Version=${BRIDGE_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/pkg/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+= ${BUILD_LDFLAGS}
endif
@ -23,39 +37,47 @@ GO_LDFLAGS:=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS+= ${GO_LDFLAGS}
BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
DEPLOY_DIR:=cmd/Desktop-Bridge/deploy
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
ICO_FILES:=
EXE:=$(shell basename ${CURDIR})
ifeq "${TARGET_OS}" "windows"
EXE:=${EXE}.exe
ICO_FILES:=logo.ico icon.rc icon_windows.syso
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
endif
ifeq "${TARGET_OS}" "darwin"
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
EXE:=${EXE}.app/Contents/MacOS/${EXE}
endif
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
ifeq "${TARGET_CMD}" "Import-Export"
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
endif
build: ${TGZ_TARGET}
build-ie:
TARGET_CMD=Import-Export $(MAKE) build
build-nogui:
go build ${BUILD_FLAGS_NOGUI} -o Desktop-Bridge cmd/Desktop-Bridge/main.go
go build ${BUILD_FLAGS_NOGUI} -o ${TARGET_CMD} cmd/${TARGET_CMD}/main.go
build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
rm -f $@
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${TARGET_OS}
${DEPLOY_DIR}/linux: ${EXE_TARGET}
cp -pf ./internal/frontend/share/icons/logo.svg ${DEPLOY_DIR}/linux/
cp -pf ./internal/frontend/share/icons/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg
cp -pf ./LICENSE ${DEPLOY_DIR}/linux/
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
${DEPLOY_DIR}/darwin: ${EXE_TARGET}
cp ./internal/frontend/share/icons/Bridge.icns ${DARWINAPP_CONTENTS}/Resources/
cp -r "utils/addcert.scpt" ${DARWINAPP_CONTENTS}/Resources/
cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${TGT_ICNS}
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
@ -63,7 +85,7 @@ ${DEPLOY_DIR}/darwin: ${EXE_TARGET}
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}"
${DEPLOY_DIR}/windows: ${EXE_TARGET}
cp ./internal/frontend/share/icons/logo.ico ${DEPLOY_DIR}/windows/
cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico
cp LICENSE ${DEPLOY_DIR}/windows/
QT_BUILD_TARGET:=build desktop
@ -75,19 +97,17 @@ endif
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
cp cmd/Desktop-Bridge/main.go .
cp cmd/${TARGET_CMD}/main.go .
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
mv deploy cmd/Desktop-Bridge
mv deploy cmd/${TARGET_CMD}
rm -rf ${TARGET_OS} main.go
logo.ico: ./internal/frontend/share/icons/logo.ico
cp $^ .
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
cp $^ $@
icon.rc: ./internal/frontend/share/icon.rc
cp $^ .
./internal/frontend/qt/icon_windows.syso: ./internal/frontend/share/icon.rc logo.ico
icon_windows.syso: icon.rc logo.ico
windres --target=pe-x86-64 -o $@ $<
icon_windows.syso: ./internal/frontend/qt/icon_windows.syso
cp $^ .
## Rules for therecipe/qt
@ -119,7 +139,7 @@ update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated
LINTVER:="v1.27.0"
LINTVER:="v1.29.0"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -157,9 +177,12 @@ test: gofiles
./internal/frontend/cli/... \
./internal/imap/... \
./internal/metrics/... \
./internal/importexport/... \
./internal/preferences/... \
./internal/smtp/... \
./internal/store/... \
./internal/transfer/... \
./internal/updates/... \
./internal/users/... \
./pkg/...
@ -173,6 +196,7 @@ coverage: test
mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager > internal/transfer/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
@ -196,15 +220,20 @@ doc:
.PHONY: gofiles
# Following files are for the whole app so it makes sense to have them in bridge package.
# (Options like cmd or internal were considered and bridge package is the best place for them.)
gofiles: ./internal/bridge/credits.go ./internal/bridge/release_notes.go
gofiles: ./internal/bridge/credits.go ./internal/bridge/release_notes.go ./internal/importexport/credits.go ./internal/importexport/release_notes.go
./internal/bridge/credits.go: ./utils/credits.sh go.mod
cd ./utils/ && ./credits.sh
./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes.txt ./release-notes/bugs.txt
cd ./utils/ && ./release-notes.sh
cd ./utils/ && ./credits.sh bridge
./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes-bridge.txt ./release-notes/bugs-bridge.txt
cd ./utils/ && ./release-notes.sh bridge
./internal/importexport/credits.go: ./utils/credits.sh go.mod
cd ./utils/ && ./credits.sh importexport
./internal/importexport/release_notes.go: ./utils/release-notes.sh ./release-notes/notes-importexport.txt ./release-notes/bugs-importexport.txt
cd ./utils/ && ./release-notes.sh importexport
## Run and debug
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug qmlpreview qt-fronted-clean clean
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug run-qml-preview run-ie-qml-preview run-ie run-ie-qt run-ie-qt-cli run-ie-nogui run-ie-nogui-cli clean-vendor clean-frontend-qt clean-frontend-qt-ie clean-frontend-qt-common clean
VERBOSITY?=debug-client
RUN_FLAGS:=-m -l=${VERBOSITY}
@ -216,24 +245,39 @@ run-qt-cli: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
run-nogui: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} | tee last.log
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log
run-nogui-cli: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} -c
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c
run-debug:
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/Desktop-Bridge/main.go -- ${RUN_FLAGS}
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS}
run-qml-preview:
make -C internal/frontend/qt -f Makefile.local qmlpreview
$(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview
run-ie-qml-preview:
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview
run-ie:
TARGET_CMD=Import-Export $(MAKE) run
run-ie-qt:
TARGET_CMD=Import-Export $(MAKE) run-qt
run-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) run-nogui
clean-frontend-qt:
make -C internal/frontend/qt -f Makefile.local clean
$(MAKE) -C internal/frontend/qt -f Makefile.local clean
clean-frontend-qt-ie:
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local clean
clean-frontend-qt-common:
$(MAKE) -C internal/frontend/qt-common -f Makefile.local clean
clean-vendor: clean-frontend-qt
clean-vendor: clean-frontend-qt clean-frontend-qt-ie clean-frontend-qt-common
rm -rf ./vendor
clean: clean-frontend-qt
clean: clean-vendor
rm -rf vendor-cache
rm -rf cmd/Desktop-Bridge/deploy
rm -f build last.log mem.pprof
rm -rf cmd/Import-Export/deploy
rm -f build last.log mem.pprof main.go
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso

View File

@ -1,4 +1,4 @@
# ProtonMail Bridge
# ProtonMail Bridge and Import Export app
Copyright (c) 2020 Proton Technologies AG
This repository holds the ProtonMail Bridge application.
@ -7,7 +7,7 @@ For licensing information see [COPYING](./COPYING.md).
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
## Description
## Description Bridge
ProtonMail Bridge for e-mail clients.
When launched, Bridge will initialize local IMAP/SMTP servers and render
@ -24,6 +24,16 @@ background.
More details [on the public website](https://protonmail.com/bridge).
## Description Import-Export app
ProtonMail Import-Export app for importing and exporting messages.
To transfer messages, firstly log in using your ProtonMail credentials.
For import, expand your account, and pick the address to which to import
messages from IMAP server or local EML or MBOX files. For export, pick
the whole account or only a specific address. Then, in both cases,
configure transfer rules (match source and target mailboxes, set time
range limits and so on) and hit start. Once the transfer is complete,
check the results.
## Keychain
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
@ -39,12 +49,13 @@ or
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
### Dev build or run
- `BRIDGE_VERSION`: set the bridge app version used during testing or building
- `APP_VERSION`: set the bridge app version used during testing or building
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
- `VERBOSITY`: set log level used during test time and by the makefile
### 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

View File

@ -1,4 +0,0 @@
FROM gitlab.protontech.ch:4567/protonmail/ci-containers/go
RUN apt-get -y update
RUN apt-get -y install openssh-client libsecret-1-dev libgl1-mesa-dev time connect-proxy

View File

@ -35,122 +35,56 @@ 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"
"github.com/ProtonMail/proton-bridge/internal/imap"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/internal/updates"
"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()
*ph.err = cli.NewExitError("Panic and restart", 666)
numberOfCrashes++
log.Error("Restarting after panic")
restartApp()
os.Exit(666)
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.
@ -158,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.
@ -176,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() != "" {
@ -192,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
@ -233,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)
@ -242,18 +168,12 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// In case user wants to do CPU or memory profiles...
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
f, err := os.Create("cpu.pprof")
if err != nil {
log.Fatal("Could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("Could not start CPU profile: ", err)
}
cmd.StartCPUProfile()
defer pprof.StopCPUProfile()
}
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
defer makeMemoryProfile()
defer cmd.MakeMemoryProfile()
}
// Now we initialize all Bridge parts.
@ -261,7 +181,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)
}
@ -273,6 +193,14 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
// Cookies must be persisted across restarts.
jar, err := cookies.NewCookieJar(pref)
if err != nil {
logrus.WithError(err).Warn("Could not create cookie jar")
} else {
cm.SetCookieJar(jar)
}
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, cm, credentialsStore)
imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance)
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
@ -329,7 +257,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
}
if frontend.IsAppRestarting() {
restartApp()
cmd.RestartApp()
}
return nil
@ -339,7 +267,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
@ -365,68 +293,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)
}
}
}

161
cmd/Import-Export/main.go Normal file
View File

@ -0,0 +1,161 @@
// 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 main
import (
"runtime/pprof"
"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/updates"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"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/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
const (
appName = "importExport"
appNameDash = "import-export-app"
)
var (
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
)
func main() {
cmd.Main(
"ProtonMail Import-Export",
"ProtonMail Import-Export app",
nil,
run,
)
}
// run initializes and starts everything in a precise order.
//
// 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(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 := &cmd.PanicHandler{
AppName: "ProtonMail Import-Export app",
Config: cfg,
Err: &contextError,
}
defer panicHandler.HandlePanic()
// First we need config and create necessary folder; it's dependency for everything.
if err := cfg.CreateDirs(); err != nil {
log.Fatal("Cannot create necessary folders: ", err)
}
// Setup of logs should be as soon as possible to ensure we record every wanted report in the log.
logLevel := context.GlobalString("log-level")
_, _ = config.SetupLog(cfg, logLevel)
// Doesn't make sense to continue when Import-Export was invoked with wrong arguments.
// We should tell that to the user before we do anything else.
if context.Args().First() != "" {
_ = cli.ShowAppHelp(context)
return cli.NewExitError("Unknown argument", 4)
}
// 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.NewImportExport(cfg.GetUpdateDir())
if dir := context.GlobalString("version-json"); 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 app is already running")
return cli.NewExitError("Import-Export app 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 {
cmd.StartCPUProfile()
defer pprof.StopCPUProfile()
}
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
defer cmd.MakeMemoryProfile()
}
// Now we initialize all Import-Export parts.
log.Debug("Initializing import-export...")
eventListener := listener.New()
events.SetupEvents(eventListener)
credentialsStore, credentialsError := credentials.NewStore(appNameDash)
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
// Different build types have different roundtrippers (e.g. we want to enable
// TLS fingerprint checks in production builds). GetRoundTripper has a different
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
importexportInstance := importexport.New(cfg, panicHandler, eventListener, cm, credentialsStore)
// Decide about frontend mode before initializing rest of import-export.
var frontendMode string
switch {
case context.GlobalBool("cli"):
frontendMode = "cli"
default:
frontendMode = "qt"
}
log.WithField("mode", frontendMode).Debug("Determined frontend mode to use")
frontend := frontend.NewImportExport(constants.Version, constants.BuildVersion, frontendMode, panicHandler, cfg, eventListener, updates, importexportInstance)
// Last part is to start everything.
log.Debug("Starting frontend...")
if err := frontend.Loop(credentialsError); err != nil {
log.Error("Frontend failed with error: ", err)
return cli.NewExitError("Frontend error", 2)
}
if frontend.IsAppRestarting() {
cmd.RestartApp()
}
return nil
}

135
doc/importexport.md Normal file
View File

@ -0,0 +1,135 @@
# Import-Export app
## Main blocks
This is basic overview of the main Import-Export blocks.
```mermaid
graph LR
S[ProtonMail server]
U[User]
subgraph "Import-Export app"
Users
Frontend["Qt / CLI"]
ImportExport
Transfer
Frontend --> ImportExport
Frontend --> Transfer
ImportExport --> Users
ImportExport --> Transfer
end
EML --> Transfer
MBOX --> Transfer
IMAP --> Transfer
S --> Transfer
Transfer --> EML
Transfer --> MBOX
Transfer --> S
U --> Frontend
```
## Code structure
More detailed graph of main types used in Import-Export app and connection between them.
```mermaid
graph TD
PM[ProtonMail Server]
EML[EML]
MBOX[MBOX]
IMAP[IMAP]
subgraph "Import-Export app"
subgraph "pkg users"
subgraph "pkg credentials"
CredStore[Store]
Creds[Credentials]
CredStore --> Creds
end
US[Users]
U[User]
US --> U
end
subgraph "pkg frontend"
CLI
Qt
end
subgraph "pkg importExport"
IE[ImportExport]
end
subgraph "pkg transfer"
Transfer
Rules
Progress
Provider
LocalProvider
EMLProvider
MBOXProvider
IMAPProvider
PMAPIProvider
Mailbox
Message
Transfer --> |source|Provider
Transfer --> |target|Provider
Transfer --> Rules
Transfer --> Progress
Provider --> LocalProvider
Provider --> EMLProvider
Provider --> MBOXProvider
Provider --> IMAPProvider
Provider --> PMAPIProvider
LocalProvider --> EMLProvider
LocalProvider --> MBOXProvider
Provider --> Mailbox
Provider --> Message
end
subgraph PMAPI
APIM[ClientManager]
APIC[Client]
APIM --> APIC
end
end
CLI --> IE
CLI --> Transfer
CLI --> Progress
Qt --> IE
Qt --> Transfer
Qt --> Progress
U --> CredStore
U --> Creds
US --> APIM
U --> APIM
PMAPIProvider --> APIM
EMLProvider --> EML
MBOXProvider --> MBOX
IMAPProvider --> IMAP
IE --> US
IE --> Transfer
APIC --> PM
```

View File

@ -2,8 +2,13 @@
Documentation pages in order to read for a novice:
* [Development cycle](development.md)
## Bridge
* [Bridge code](bridge.md)
* [Internal Bridge database](database.md)
* [Communication between Bridge, Client and Server](communication.md)
* [Encryption](encryption.md)
## Import-Export app
* [Import-Export code](importexport.md)

49
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/docker/docker-credential-helpers v0.6.3
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
)
require (
@ -22,7 +22,7 @@ require (
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
github.com/andybalholm/cascadia v1.1.0
github.com/andybalholm/cascadia v1.2.0
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
@ -32,37 +32,46 @@ require (
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
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-sasl v0.0.0-20191210011802-430746ea8b9b
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
github.com/fatih/color v1.9.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/getsentry/raven-go v0.2.0
github.com/go-resty/resty/v2 v2.2.0
github.com/golang/mock v1.4.3
github.com/google/go-cmp v0.4.0
github.com/go-resty/resty/v2 v2.3.0
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1
github.com/google/uuid v1.1.1
github.com/go-delve/delve v1.4.1 // indirect
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
github.com/hashicorp/go-multierror v1.0.0
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195
github.com/jhillyerd/enmime v0.8.0
github.com/hashicorp/go-multierror v1.1.0
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
github.com/jhillyerd/enmime v0.8.1
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
github.com/miekg/dns v1.1.29
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.30
github.com/myesui/uuid v1.0.0 // indirect
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.4.2
github.com/psampaz/go-mod-outdated v0.6.0 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.5.1
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41
github.com/stretchr/testify v1.6.1
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22 // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22 // indirect
github.com/twinj/uuid v1.0.0 // indirect
github.com/urfave/cli v1.22.3
github.com/urfave/cli v1.22.4
go.etcd.io/bbolt v1.3.5
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
golang.org/x/text v0.3.2
golang.org/x/net v0.0.0-20200707034311-ab3426394381
golang.org/x/text v0.3.3
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
)

121
go.sum
View File

@ -29,8 +29,8 @@ github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLb
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
@ -39,8 +39,12 @@ github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8=
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
@ -56,14 +60,18 @@ github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1Po
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU=
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 h1:z8TiDE4yqtzNeA1yb6ZRcktd+BHlXQbKGugvmDuc488=
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.0 h1:HN6aKbyqmgIfK9fS/gen+NRr2wXLSxZXWfdAIAnzQPc=
github.com/emersion/go-mbox v1.0.0/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
@ -74,61 +82,101 @@ github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWT
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
github.com/go-delve/delve v1.4.1 h1:kZs0umEv+VKnK84kY9/ZXWrakdLTeRTyYjFdgLelZCQ=
github.com/go-delve/delve v1.4.1/go.mod h1:vmy6iObn7zg8FQ5KOCIe6TruMNsqpoZO8uMiRea+97k=
github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195 h1:j0UEFmS7wSjAwKEIkgKBn8PRDfjcuggzr93R9wk53nQ=
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.8.0 h1:PHc/2LXtnDmCDm0V4+5NlBx+MoubmufhuNXwpKSV2o8=
github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.8.1 h1:Kz4xj3sJJ4Ju8e+w/7v9H4Matv5ijPgv7UkhPf+C15I=
github.com/jhillyerd/enmime v0.8.1/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2 h1:1XZArHAPddaXKbg51etNbCjkNUkKgSa0s8dSz2LYB2g=
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6 h1:qsqscDgSJy+HqgMTR+3NwjYJBbp1+honwDsszLoS+pA=
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/psampaz/go-mod-outdated v0.6.0 h1:DXS6rdsz4rpezbPsckQflqrYSEBvsF5GAmUWP+UvnQo=
github.com/psampaz/go-mod-outdated v0.6.0/go.mod h1:r78NYWd1z+F9Zdsfy70svgXOz363B08BWnTyFSgEESs=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
@ -138,8 +186,12 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -151,14 +203,30 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200603231648-26cdb75b6f22 h1:UrNr8EZueA1eREFmG5gVHBeeOuwW2GbzI9VfdB5uK+c=
github.com/therecipe/qt/internal/binding/files/docs v0.0.0-20191019224306-1097424d656c h1:/VhcwU7WuFEVgDHZ9V8PIYAyYqQ6KNxFUjBMOf2aFZM=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22 h1:FumuOkCw78iheUI3eIYhAgtsj/0HQBAib/jXk1cslJw=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22 h1:aYzTBQ/hC6FtbaRnyylxlhbSGMPnyD5lAzVO3Ae6emA=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -167,9 +235,9 @@ golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -178,28 +246,41 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/listener"
logrus "github.com/sirupsen/logrus"
@ -60,7 +61,7 @@ func New(
}
storeFactory := newStoreFactory(config, panicHandler, clientManager, eventListener)
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory)
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
b := &Bridge{
Users: u,
@ -70,6 +71,7 @@ func New(
if pref.GetBool(preferences.FirstStartKey) {
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
pref.SetBool(preferences.FirstStartKey, false)
}
go b.heartbeat()
@ -129,15 +131,17 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
defer c.Logout()
title := "[Bridge] Bug"
if err := c.ReportBugWithEmailClient(
osType,
osVersion,
title,
description,
accountName,
address,
emailClient,
); err != nil {
report := pmapi.ReportReq{
OS: osType,
OSVersion: osVersion,
Browser: emailClient,
Title: title,
Description: description,
Username: accountName,
Email: address,
}
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err)
return err
}

View File

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

View File

@ -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 Wed 29 Jul 2020 07:07:28 AM CEST. DO NOT EDIT.
// Code generated by ./release-notes.sh at 'Fri 04 Sep 2020 01:57:36 PM CEST'. DO NOT EDIT.
package bridge

View File

@ -32,6 +32,7 @@ type StoreFactoryConfiger interface {
type PreferenceProvider interface {
Get(key string) string
GetBool(key string) bool
SetBool(key string, val bool)
GetInt(key string) int
Set(key string, value string)
}

View File

@ -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
View 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
}

54
internal/cmd/profiles.go Normal file
View File

@ -0,0 +1,54 @@
// 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"
)
// StartCPUProfile starts CPU pprof.
func StartCPUProfile() {
f, err := os.Create("./cpu.pprof")
if err != nil {
log.Fatal("Could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("Could not start CPU profile: ", err)
}
}
// MakeMemoryProfile generates memory pprof.
func MakeMemoryProfile() {
name := "./mem.pprof"
f, err := os.Create(name)
if err != nil {
log.Fatal("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.Fatal("Could not write memory profile: ", err)
}
_ = f.Close()
}

108
internal/cmd/restart.go Normal file
View File

@ -0,0 +1,108 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package 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)
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cmd
import "github.com/ProtonMail/proton-bridge/internal/updates"
// GenerateVersionFiles writes a JSON file with details about current build.
// Those files are used for upgrading the app.
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)
}
}
}

92
internal/cookies/jar.go Normal file
View File

@ -0,0 +1,92 @@
// 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 cookies implements a persistent cookie jar which satisfies the http.CookieJar interface.
package cookies
import (
"net/http"
"net/http/cookiejar"
"net/url"
"sync"
"github.com/sirupsen/logrus"
)
// Jar implements http.CookieJar by wrapping the standard library's cookiejar.Jar.
// The jar uses a pantry to load cookies at startup and save cookies when set.
type Jar struct {
jar *cookiejar.Jar
pantry *pantry
locker sync.Locker
}
type GetterSetter interface {
Get(string) string
Set(string, string)
}
func NewCookieJar(gs GetterSetter) (*Jar, error) {
pantry := &pantry{gs: gs}
if err := pantry.discardExpiredCookies(); err != nil {
return nil, err
}
cookies, err := pantry.loadFromJSON()
if err != nil {
return nil, err
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
for rawURL, cookies := range cookies {
url, err := url.Parse(rawURL)
if err != nil {
continue
}
jar.SetCookies(url, cookies)
}
return &Jar{
jar: jar,
pantry: pantry,
locker: &sync.Mutex{},
}, nil
}
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.locker.Lock()
defer j.locker.Unlock()
j.jar.SetCookies(u, cookies)
if err := j.pantry.persistCookies(u.Scheme+"://"+u.Host, cookies); err != nil {
logrus.WithError(err).Warn("Failed to persist cookie")
}
}
func (j *Jar) Cookies(u *url.URL) []*http.Cookie {
j.locker.Lock()
defer j.locker.Unlock()
return j.jar.Cookies(u)
}

View File

@ -0,0 +1,168 @@
// 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 cookies
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJarGetSet(t *testing.T) {
ts := getTestServer(t, []testCookie{
{"TestName1", "TestValue1", 3600},
{"TestName2", "TestValue2", 3600},
{"TestName3", "TestValue3", 3600},
})
defer ts.Close()
client := getClientWithJar(t, make(testGetterSetter))
// Hit a server that sets some cookies.
setRes, err := client.Get(ts.URL + "/set")
if err != nil {
t.FailNow()
}
require.NoError(t, setRes.Body.Close())
// Hit a server that checks the cookies are there.
getRes, err := client.Get(ts.URL + "/get")
if err != nil {
t.FailNow()
}
require.NoError(t, getRes.Body.Close())
}
func TestJarLoad(t *testing.T) {
ts := getTestServer(t, []testCookie{
{"TestName1", "TestValue1", 3600},
{"TestName2", "TestValue2", 3600},
{"TestName3", "TestValue3", 3600},
})
defer ts.Close()
// This will be our "persistent storage" from which the cookie jar should load cookies.
gs := make(testGetterSetter)
// This client saves cookies to persistent storage.
oldClient := getClientWithJar(t, gs)
// Hit a server that sets some cookies.
setRes, err := oldClient.Get(ts.URL + "/set")
if err != nil {
t.FailNow()
}
require.NoError(t, setRes.Body.Close())
// This client loads cookies from persistent storage.
newClient := getClientWithJar(t, gs)
// Hit a server that checks the cookies are there.
getRes, err := newClient.Get(ts.URL + "/get")
if err != nil {
t.FailNow()
}
require.NoError(t, getRes.Body.Close())
}
func TestJarExpiry(t *testing.T) {
ts := getTestServer(t, []testCookie{
{"TestName1", "TestValue1", 3600},
{"TestName2", "TestValue2", 1},
{"TestName3", "TestValue3", 3600},
})
defer ts.Close()
// This will be our "persistent storage" from which the cookie jar should load cookies.
gs := make(testGetterSetter)
// This client saves cookies to persistent storage.
oldClient := getClientWithJar(t, gs)
// Hit a server that sets some cookies.
setRes, err := oldClient.Get(ts.URL + "/set")
if err != nil {
t.FailNow()
}
require.NoError(t, setRes.Body.Close())
// Wait until the second cookie expires.
time.Sleep(2 * time.Second)
// Load a client, which will clear out expired cookies.
_ = getClientWithJar(t, gs)
assert.Contains(t, gs["cookies"], "TestName1")
assert.NotContains(t, gs["cookies"], "TestName2")
assert.Contains(t, gs["cookies"], "TestName3")
}
type testCookie struct {
name, value string
maxAge int
}
func getClientWithJar(t *testing.T, gs GetterSetter) *http.Client {
jar, err := NewCookieJar(gs)
require.NoError(t, err)
return &http.Client{Jar: jar}
}
func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/set", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, cookie := range wantCookies {
http.SetCookie(w, &http.Cookie{
Name: cookie.name,
Value: cookie.value,
MaxAge: cookie.maxAge,
})
}
w.WriteHeader(http.StatusOK)
}))
mux.HandleFunc("/get", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Len(t, r.Cookies(), len(wantCookies))
for k, v := range r.Cookies() {
assert.Equal(t, wantCookies[k].name, v.Name)
assert.Equal(t, wantCookies[k].value, v.Value)
}
w.WriteHeader(http.StatusOK)
}))
return httptest.NewServer(mux)
}
type testGetterSetter map[string]string
func (p testGetterSetter) Set(key, value string) {
p[key] = value
}
func (p testGetterSetter) Get(key string) string {
return p[key]
}

100
internal/cookies/pantry.go Normal file
View File

@ -0,0 +1,100 @@
// 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 cookies
import (
"encoding/json"
"net/http"
"time"
"github.com/ProtonMail/proton-bridge/internal/preferences"
)
// pantry persists and loads cookies to some persistent storage location.
type pantry struct {
gs GetterSetter
}
func (p *pantry) persistCookies(host string, cookies []*http.Cookie) error {
for _, cookie := range cookies {
if cookie.MaxAge > 0 {
cookie.Expires = time.Now().Add(time.Duration(cookie.MaxAge) * time.Second)
}
}
cookiesByHost, err := p.loadFromJSON()
if err != nil {
return err
}
cookiesByHost[host] = cookies
return p.saveToJSON(cookiesByHost)
}
func (p *pantry) discardExpiredCookies() error {
cookiesByHost, err := p.loadFromJSON()
if err != nil {
return err
}
for host, cookies := range cookiesByHost {
cookiesByHost[host] = discardExpiredCookies(cookies)
}
return p.saveToJSON(cookiesByHost)
}
type cookiesByHost map[string][]*http.Cookie
func (p *pantry) loadFromJSON() (cookiesByHost, error) {
b := p.gs.Get(preferences.CookiesKey)
if b == "" {
return make(cookiesByHost), nil
}
var cookies cookiesByHost
if err := json.Unmarshal([]byte(b), &cookies); err != nil {
return nil, err
}
return cookies, nil
}
func (p *pantry) saveToJSON(cookies cookiesByHost) error {
b, err := json.Marshal(cookies)
if err != nil {
return err
}
p.gs.Set(preferences.CookiesKey, string(b))
return nil
}
func discardExpiredCookies(cookies []*http.Cookie) (validCookies []*http.Cookie) {
for _, cookie := range cookies {
if cookie.Expires.After(time.Now()) {
validCookies = append(validCookies, cookie)
}
}
return
}

View File

@ -43,7 +43,7 @@ func (c *appleMail) Name() string {
return "Apple Mail"
}
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.BridgeUser, addressIndex int) error { //nolint[funlen]
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) error { //nolint[funlen]
var addresses string
var displayName string

View File

@ -23,7 +23,7 @@ import "github.com/ProtonMail/proton-bridge/internal/frontend/types"
type AutoConfig interface {
Name() string
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.BridgeUser, addressIndex int) error
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.User, addressIndex int) error
}
var available []AutoConfig //nolint[gochecknoglobals]

View File

@ -0,0 +1,100 @@
// 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 cliie
import (
"fmt"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/abiosoft/ishell"
)
// completeUsernames is a helper to complete usernames as the user types.
func (f *frontendCLI) completeUsernames(args []string) (usernames []string) {
if len(args) > 1 {
return
}
arg := ""
if len(args) == 1 {
arg = args[0]
}
for _, user := range f.ie.GetUsers() {
if strings.HasPrefix(strings.ToLower(user.Username()), strings.ToLower(arg)) {
usernames = append(usernames, user.Username())
}
}
return
}
// noAccountWrapper is a decorator for functions which need any account to be properly functional.
func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ishell.Context) {
return func(c *ishell.Context) {
users := f.ie.GetUsers()
if len(users) == 0 {
f.Println("No active accounts. Please add account to continue.")
} else {
callback(c)
}
}
}
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.User {
user := f.getUserByIndexOrName("")
if user != nil {
return user
}
numberOfAccounts := len(f.ie.GetUsers())
indexRange := fmt.Sprintf("number between 0 and %d", numberOfAccounts-1)
if len(c.Args) == 0 {
f.Printf("Please choose %s or username.\n", indexRange)
return nil
}
arg := c.Args[0]
user = f.getUserByIndexOrName(arg)
if user == nil {
f.Printf("Wrong input '%s'. Choose %s or username.\n", bold(arg), indexRange)
return nil
}
return user
}
func (f *frontendCLI) getUserByIndexOrName(arg string) types.User {
users := f.ie.GetUsers()
numberOfAccounts := len(users)
if numberOfAccounts == 0 {
return nil
}
if numberOfAccounts == 1 {
return users[0]
}
if index, err := strconv.Atoi(arg); err == nil {
if index < 0 || index >= numberOfAccounts {
return nil
}
return users[index]
}
for _, user := range users {
if user.Username() == arg {
return user
}
}
return nil
}

View File

@ -0,0 +1,153 @@
// 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 cliie
import (
"strings"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) listAccounts(c *ishell.Context) {
spacing := "%-2d: %-20s (%-15s, %-15s)\n"
f.Printf(bold(strings.Replace(spacing, "d", "s", -1)), "#", "account", "status", "address mode")
for idx, user := range f.ie.GetUsers() {
connected := "disconnected"
if user.IsConnected() {
connected = "connected"
}
mode := "split"
if user.IsCombinedAddressMode() {
mode = "combined"
}
f.Printf(spacing, idx, user.Username(), connected, mode)
}
f.Println()
}
func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
f.ShowPrompt(false)
defer f.ShowPrompt(true)
loginName := ""
if len(c.Args) > 0 {
user := f.getUserByIndexOrName(c.Args[0])
if user != nil {
loginName = user.GetPrimaryAddress()
}
}
if loginName == "" {
loginName = f.readStringInAttempts("Username", c.ReadLine, isNotEmpty)
if loginName == "" {
return
}
} else {
f.Println("Username:", loginName)
}
password := f.readStringInAttempts("Password", c.ReadPassword, isNotEmpty)
if password == "" {
return
}
f.Println("Authenticating ... ")
client, auth, err := f.ie.Login(loginName, password)
if err != nil {
f.processAPIError(err)
return
}
if auth.HasTwoFactor() {
twoFactor := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
if twoFactor == "" {
return
}
_, err = client.Auth2FA(twoFactor, auth)
if err != nil {
f.processAPIError(err)
return
}
}
mailboxPassword := password
if auth.HasMailboxPassword() {
mailboxPassword = f.readStringInAttempts("Mailbox password", c.ReadPassword, isNotEmpty)
}
if mailboxPassword == "" {
return
}
f.Println("Adding account ...")
user, err := f.ie.FinishLogin(client, auth, mailboxPassword)
if err != nil {
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
f.Println("Adding account was unsuccessful:", err)
return
}
f.Printf("Account %s was added successfully.\n", bold(user.Username()))
}
func (f *frontendCLI) logoutAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
if f.yesNoQuestion("Are you sure you want to logout account " + bold(user.Username())) {
if err := user.Logout(); err != nil {
f.printAndLogError("Logging out failed: ", err)
}
}
}
func (f *frontendCLI) deleteAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
if f.yesNoQuestion("Are you sure you want to " + bold("remove account "+user.Username())) {
clearCache := f.yesNoQuestion("Do you want to remove cache for this account")
if err := f.ie.DeleteUser(user.ID(), clearCache); err != nil {
f.printAndLogError("Cannot delete account: ", err)
return
}
}
}
func (f *frontendCLI) deleteAccounts(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
if !f.yesNoQuestion("Do you really want remove all accounts") {
return
}
for _, user := range f.ie.GetUsers() {
if err := f.ie.DeleteUser(user.ID(), false); err != nil {
f.printAndLogError("Cannot delete account ", user.Username(), ": ", err)
}
}
c.Println("Keychain cleared")
}

View File

@ -0,0 +1,237 @@
// 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 cliie provides CLI interface of the Import-Export app.
package cliie
import (
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/abiosoft/ishell"
"github.com/sirupsen/logrus"
)
var (
log = logrus.WithField("pkg", "frontend/cli-ie") //nolint[gochecknoglobals]
)
type frontendCLI struct {
*ishell.Shell
config *config.Config
eventListener listener.Listener
updates types.Updater
ie types.ImportExporter
appRestart bool
}
// New returns a new CLI frontend configured with the given options.
func New( //nolint[funlen]
panicHandler types.PanicHandler,
config *config.Config,
eventListener listener.Listener,
updates types.Updater,
ie types.ImportExporter,
) *frontendCLI { //nolint[golint]
fe := &frontendCLI{
Shell: ishell.New(),
config: config,
eventListener: eventListener,
updates: updates,
ie: ie,
appRestart: false,
}
// Clear commands.
clearCmd := &ishell.Cmd{Name: "clear",
Help: "remove stored accounts and preferences. (alias: cl)",
Aliases: []string{"cl"},
}
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts,
})
fe.AddCmd(clearCmd)
// Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
Help: "check for Import-Export updates. (aliases: u, v, version)",
Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates,
})
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"},
Func: fe.checkInternetConnection,
})
fe.AddCmd(checkCmd)
// Print info commands.
fe.AddCmd(&ishell.Cmd{Name: "log-dir",
Help: "print path to directory with logs. (aliases: log, logs)",
Aliases: []string{"log", "logs"},
Func: fe.printLogDir,
})
fe.AddCmd(&ishell.Cmd{Name: "manual",
Help: "print URL with instructions. (alias: man)",
Aliases: []string{"man"},
Func: fe.printManual,
})
fe.AddCmd(&ishell.Cmd{Name: "release-notes",
Help: "print release notes. (aliases: notes, fixed-bugs, bugs, ver, version)",
Aliases: []string{"notes", "fixed-bugs", "bugs", "ver", "version"},
Func: fe.printLocalReleaseNotes,
})
fe.AddCmd(&ishell.Cmd{Name: "credits",
Help: "print used resources.",
Func: fe.printCredits,
})
// Account commands.
fe.AddCmd(&ishell.Cmd{Name: "list",
Help: "print the list of accounts. (aliases: l, ls)",
Func: fe.noAccountWrapper(fe.listAccounts),
Aliases: []string{"l", "ls"},
})
fe.AddCmd(&ishell.Cmd{Name: "login",
Help: "login procedure to add or connect account. Optionally use index or account as parameter. (aliases: a, add, con, connect)",
Func: fe.loginAccount,
Aliases: []string{"add", "a", "con", "connect"},
Completer: fe.completeUsernames,
})
fe.AddCmd(&ishell.Cmd{Name: "logout",
Help: "disconnect the account. Use index or account name as parameter. (aliases: d, disconnect)",
Func: fe.noAccountWrapper(fe.logoutAccount),
Aliases: []string{"d", "disconnect"},
Completer: fe.completeUsernames,
})
fe.AddCmd(&ishell.Cmd{Name: "delete",
Help: "remove the account from keychain. Use index or account name as parameter. (aliases: del, rm, remove)",
Func: fe.noAccountWrapper(fe.deleteAccount),
Aliases: []string{"del", "rm", "remove"},
Completer: fe.completeUsernames,
})
// Import-Export commands.
importCmd := &ishell.Cmd{Name: "import",
Help: "import messages. (alias: imp)",
Aliases: []string{"imp"},
}
importCmd.AddCmd(&ishell.Cmd{Name: "local",
Help: "import local messages. (aliases: loc)",
Func: fe.noAccountWrapper(fe.importLocalMessages),
Aliases: []string{"loc"},
})
importCmd.AddCmd(&ishell.Cmd{Name: "remote",
Help: "import remote messages. (aliases: rem)",
Func: fe.noAccountWrapper(fe.importRemoteMessages),
Aliases: []string{"rem"},
})
fe.AddCmd(importCmd)
exportCmd := &ishell.Cmd{Name: "export",
Help: "export messages. (alias: exp)",
Aliases: []string{"exp"},
}
exportCmd.AddCmd(&ishell.Cmd{Name: "eml",
Help: "export messages to eml files.",
Func: fe.noAccountWrapper(fe.exportMessagesToEML),
})
exportCmd.AddCmd(&ishell.Cmd{Name: "mbox",
Help: "export messages to mbox files.",
Func: fe.noAccountWrapper(fe.exportMessagesToMBOX),
})
fe.AddCmd(exportCmd)
// System commands.
fe.AddCmd(&ishell.Cmd{Name: "restart",
Help: "restart the Import-Export app.",
Func: fe.restart,
})
go func() {
defer panicHandler.HandlePanic()
fe.watchEvents()
}()
fe.eventListener.RetryEmit(events.TLSCertIssue)
fe.eventListener.RetryEmit(events.ErrorEvent)
return fe
}
func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue)
for {
select {
case errorDetails := <-errorCh:
f.Println("Import-Export failed:", errorDetails)
case <-internetOffCh:
f.notifyInternetOff()
case <-internetOnCh:
f.notifyInternetOn()
case address := <-addressChangedLogoutCh:
f.notifyLogout(address)
case userID := <-logoutCh:
user, err := f.ie.GetUser(userID)
if err != nil {
return
}
f.notifyLogout(user.Username())
case <-certIssue:
f.notifyCertIssue()
}
}
}
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
return ch
}
// IsAppRestarting returns whether the app is currently set to restart.
func (f *frontendCLI) IsAppRestarting() bool {
return f.appRestart
}
// Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop(credentialsError error) error {
if credentialsError != nil {
f.notifyCredentialsError()
return credentialsError
}
f.Print(`
Welcome to ProtonMail Import-Export app interactive shell
WARNING: The CLI is an experimental feature and does not yet cover all functionality.
`)
f.Run()
return nil
}

View File

@ -0,0 +1,224 @@
// 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 cliie
import (
"fmt"
"os"
"strings"
"time"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) importLocalMessages(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user, path := f.getUserAndPath(c, false)
if user == nil || path == "" {
return
}
t, err := f.ie.GetLocalImporter(user.GetPrimaryAddress(), path)
f.transfer(t, err, false, true)
}
func (f *frontendCLI) importRemoteMessages(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
username := f.readStringInAttempts("IMAP username", c.ReadLine, isNotEmpty)
if username == "" {
return
}
password := f.readStringInAttempts("IMAP password", c.ReadPassword, isNotEmpty)
if password == "" {
return
}
host := f.readStringInAttempts("IMAP host", c.ReadLine, isNotEmpty)
if host == "" {
return
}
port := f.readStringInAttempts("IMAP port", c.ReadLine, isNotEmpty)
if port == "" {
return
}
t, err := f.ie.GetRemoteImporter(user.GetPrimaryAddress(), username, password, host, port)
f.transfer(t, err, false, true)
}
func (f *frontendCLI) exportMessagesToEML(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user, path := f.getUserAndPath(c, true)
if user == nil || path == "" {
return
}
t, err := f.ie.GetEMLExporter(user.GetPrimaryAddress(), path)
f.transfer(t, err, true, false)
}
func (f *frontendCLI) exportMessagesToMBOX(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user, path := f.getUserAndPath(c, true)
if user == nil || path == "" {
return
}
t, err := f.ie.GetMBOXExporter(user.GetPrimaryAddress(), path)
f.transfer(t, err, true, false)
}
func (f *frontendCLI) getUserAndPath(c *ishell.Context, createPath bool) (types.User, string) {
user := f.askUserByIndexOrName(c)
if user == nil {
return nil, ""
}
path := f.readStringInAttempts("Path of EML and MBOX files", c.ReadLine, isNotEmpty)
if path == "" {
return nil, ""
}
if createPath {
_ = os.Mkdir(path, os.ModePerm)
}
return user, path
}
func (f *frontendCLI) transfer(t *transfer.Transfer, err error, askSkipEncrypted bool, askGlobalMailbox bool) {
if err != nil {
f.printAndLogError("Failed to init transferrer: ", err)
return
}
if askSkipEncrypted {
skipEncryptedMessages := f.yesNoQuestion("Skip encrypted messages")
t.SetSkipEncryptedMessages(skipEncryptedMessages)
}
if !f.setTransferRules(t) {
return
}
if askGlobalMailbox {
if err := f.setTransferGlobalMailbox(t); err != nil {
f.printAndLogError("Failed to create global mailbox: ", err)
return
}
}
progress := t.Start()
for range progress.GetUpdateChannel() {
f.printTransferProgress(progress)
}
f.printTransferResult(progress)
}
func (f *frontendCLI) setTransferGlobalMailbox(t *transfer.Transfer) error {
labelName := fmt.Sprintf("Imported %s", time.Now().Format("Jan-02-2006 15:04"))
useGlobalLabel := f.yesNoQuestion("Use global label " + labelName)
if !useGlobalLabel {
return nil
}
globalMailbox, err := t.CreateTargetMailbox(transfer.Mailbox{
Name: labelName,
Color: pmapi.LabelColors[0],
IsExclusive: false,
})
if err != nil {
return err
}
t.SetGlobalMailbox(&globalMailbox)
return nil
}
func (f *frontendCLI) setTransferRules(t *transfer.Transfer) bool {
f.Println("Rules:")
for _, rule := range t.GetRules() {
if !rule.Active {
continue
}
targets := strings.Join(rule.TargetMailboxNames(), ", ")
if rule.HasTimeLimit() {
f.Printf(" %-30s → %s (%s - %s)\n", rule.SourceMailbox.Name, targets, rule.FromDate(), rule.ToDate())
} else {
f.Printf(" %-30s → %s\n", rule.SourceMailbox.Name, targets)
}
}
return f.yesNoQuestion("Proceed")
}
func (f *frontendCLI) printTransferProgress(progress *transfer.Progress) {
failed, imported, exported, added, total := progress.GetCounts()
if total != 0 {
f.Println(fmt.Sprintf("Progress update: %d (%d / %d) / %d, failed: %d", imported, exported, added, total, failed))
}
if progress.IsPaused() {
f.Printf("Transfer is paused bacause %s", progress.PauseReason())
if !f.yesNoQuestion("Continue (y) or stop (n)") {
progress.Stop()
}
}
}
func (f *frontendCLI) printTransferResult(progress *transfer.Progress) {
err := progress.GetFatalError()
if err != nil {
f.Println("Transfer failed: " + err.Error())
return
}
statuses := progress.GetFailedMessages()
if len(statuses) == 0 {
f.Println("Transfer finished!")
return
}
f.Println("Transfer finished with errors:")
for _, messageStatus := range statuses {
f.Printf(
" %-17s | %-30s | %-30s\n %s: %s\n",
messageStatus.Time.Format("Jan 02 2006 15:04"),
messageStatus.From,
messageStatus.Subject,
messageStatus.SourceID,
messageStatus.GetErrorMessage(),
)
}
}

View File

@ -0,0 +1,46 @@
// 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 cliie
import (
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) restart(c *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to restart the Import-Export app") {
f.Println("Restarting the Import-Export app...")
f.appRestart = true
f.Stop()
}
}
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.ie.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check your internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) {
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
}
func (f *frontendCLI) printManual(c *ishell.Context) {
f.Println("More instructions about the Import-Export app can be found at\n\n https://protonmail.com/support/categories/import-export/")
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cliie
import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
if err != nil {
f.printAndLogError("Cannot retrieve version info: ", err)
f.checkInternetConnection(c)
return
}
if isUpToDate {
f.Println("Your version is up to date.")
} else {
f.notifyNeedUpgrade()
f.Println("")
f.printReleaseNotes(latestVersionInfo)
}
}
func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) {
localVersion := f.updates.GetLocalVersion()
f.printReleaseNotes(localVersion)
}
func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
f.Println(bold("ProtonMail Import-Export app "+versionInfo.Version), "\n")
if versionInfo.ReleaseNotes != "" {
f.Println(bold("Release Notes"))
f.Println(versionInfo.ReleaseNotes)
}
if versionInfo.ReleaseFixedBugs != "" {
f.Println(bold("Fixed bugs"))
f.Println(versionInfo.ReleaseFixedBugs)
}
}
func (f *frontendCLI) printCredits(c *ishell.Context) {
for _, pkg := range strings.Split(importexport.Credits, ";") {
f.Println(pkg)
}
}

View File

@ -0,0 +1,123 @@
// 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 cliie
import (
"strings"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/fatih/color"
)
const (
maxInputRepeat = 2
)
var (
bold = color.New(color.Bold).SprintFunc() //nolint[gochecknoglobals]
)
func isNotEmpty(val string) bool {
return val != ""
}
func (f *frontendCLI) yesNoQuestion(question string) bool {
f.Print(question, "? yes/"+bold("no")+": ")
yes := "yes"
answer := strings.ToLower(f.ReadLine())
for i := 0; i < len(answer); i++ {
if i >= len(yes) || answer[i] != yes[i] {
return false // Everything else is false.
}
}
return len(answer) > 0 // Empty is false.
}
func (f *frontendCLI) readStringInAttempts(title string, readFunc func() string, isOK func(string) bool) (value string) {
f.Printf("%s: ", title)
value = readFunc()
title = strings.ToLower(string(title[0])) + title[1:]
for i := 0; !isOK(value); i++ {
if i >= maxInputRepeat {
f.Println("Too many attempts")
return ""
}
f.Printf("Please fill %s: ", title)
value = readFunc()
}
return
}
func (f *frontendCLI) printAndLogError(args ...interface{}) {
log.Error(args...)
f.Println(args...)
}
func (f *frontendCLI) processAPIError(err error) {
log.Warn("API error: ", err)
switch err {
case pmapi.ErrAPINotReachable:
f.notifyInternetOff()
case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade()
default:
f.Println("Server error:", err.Error())
}
}
func (f *frontendCLI) notifyInternetOff() {
f.Println("Internet connection is not available.")
}
func (f *frontendCLI) notifyInternetOn() {
f.Println("Internet connection is available again.")
}
func (f *frontendCLI) notifyLogout(address string) {
f.Printf("Account %s is disconnected. Login to continue using this account with email client.", address)
}
func (f *frontendCLI) notifyNeedUpgrade() {
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
}
func (f *frontendCLI) notifyCredentialsError() {
// Print in 80-column width.
f.Println("ProtonMail Import-Export app 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.")
}
func (f *frontendCLI) notifyCertIssue() {
// Print in 80-column width.
f.Println(`Connection security error: Your network connection to Proton services may
be insecure.
Description:
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.
Recommendation:
* If you trust your network operator, you can continue to use ProtonMail
as usual.
* If you don't trust your network operator, reconnect to ProtonMail over a VPN
(such as ProtonVPN) which encrypts your Internet connection, or use
a different network to access ProtonMail.
`)
}

View File

@ -55,7 +55,7 @@ func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ish
}
}
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.User {
user := f.getUserByIndexOrName("")
if user != nil {
return user
@ -76,7 +76,7 @@ func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
return user
}
func (f *frontendCLI) getUserByIndexOrName(arg string) types.BridgeUser {
func (f *frontendCLI) getUserByIndexOrName(arg string) types.User {
users := f.bridge.GetUsers()
numberOfAccounts := len(users)
if numberOfAccounts == 0 {

View File

@ -63,7 +63,7 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
}
}
func (f *frontendCLI) showAccountAddressInfo(user types.BridgeUser, address string) {
func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) {
smtpSecurity := "STARTTLS"
if f.preferences.GetBool(preferences.SMTPSSLKey) {
smtpSecurity = "SSL"

View File

@ -21,7 +21,6 @@ package cli
import (
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
@ -77,7 +76,7 @@ func New( //nolint[funlen]
Func: fe.deleteCache,
})
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: k, keychain)",
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts,
})
@ -241,8 +240,6 @@ func (f *frontendCLI) Loop(credentialsError error) error {
return credentialsError
}
f.preferences.SetBool(preferences.FirstStartKey, false)
f.Print(`
Welcome to ProtonMail Bridge interactive shell
___....___

View File

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

View File

@ -21,12 +21,12 @@ import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
isUpToDate, latestVersionInfo, err := f.updates.CheckIsBridgeUpToDate()
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
if err != nil {
f.printAndLogError("Cannot retrieve version info: ", err)
f.checkInternetConnection(c)

View File

@ -22,8 +22,11 @@ import (
"github.com/0xAX/notificator"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
qtie "github.com/ProtonMail/proton-bridge/internal/frontend/qt-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
@ -40,12 +43,12 @@ type Frontend interface {
}
// HandlePanic handles panics which occur for users with GUI.
func HandlePanic() {
func HandlePanic(appName string) {
notify := notificator.New(notificator.Options{
DefaultIcon: "../frontend/ui/icon/icon.png",
AppName: "ProtonMail Bridge",
AppName: appName,
})
_ = notify.Push("Fatal Error", "The ProtonMail Bridge has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
_ = notify.Push("Fatal Error", "The "+appName+" has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
}
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
@ -86,3 +89,36 @@ func new(
return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
}
}
// NewImportExport returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
func NewImportExport(
version,
buildVersion,
frontendType string,
panicHandler types.PanicHandler,
config *config.Config,
eventListener listener.Listener,
updates types.Updater,
ie *importexport.ImportExport,
) Frontend {
ieWrap := types.NewImportExportWrap(ie)
return newImportExport(version, buildVersion, frontendType, panicHandler, config, eventListener, updates, ieWrap)
}
func newImportExport(
version,
buildVersion,
frontendType string,
panicHandler types.PanicHandler,
config *config.Config,
eventListener listener.Listener,
updates types.Updater,
ie types.ImportExporter,
) Frontend {
switch frontendType {
case "cli":
return cliie.New(panicHandler, config, eventListener, updates, ie)
default:
return qtie.New(version, buildVersion, panicHandler, config, eventListener, updates, ie)
}
}

View File

@ -84,7 +84,7 @@ Window {
anchors.horizontalCenter: parent.horizontalCenter
ButtonRounded {
id: cancel
id: sendAnyway
onClicked : root.hide(true)
height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3
@ -93,7 +93,7 @@ Window {
}
ButtonRounded {
id: sendAnyway
id: cancel
onClicked : root.hide(false)
height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3

View File

@ -0,0 +1,418 @@
// 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/>.
// This is main qml file
import QtQuick 2.8
import ImportExportUI 1.0
import ProtonUI 1.0
// All imports from dynamic must be loaded before
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
Item {
id: gui
property alias winMain: winMain
property bool isFirstWindow: true
property int warningFlags: 0
property var locale : Qt.locale("en_US")
property date netBday : new Date("1989-03-13T00:00:00")
property var allYears : getYearList(1970,(new Date()).getFullYear())
property var allMonths : getMonthList(1,12)
property var allDays : getDayList(1,31)
property var enums : JSON.parse('{"pathOK":1,"pathEmptyPath":2,"pathWrongPath":4,"pathNotADir":8,"pathWrongPermissions":16,"pathDirEmpty":32,"errUnknownError":0,"errEventAPILogout":1,"errUpdateAPI":2,"errUpdateJSON":3,"errUserAuth":4,"errQApplication":18,"errEmailExportFailed":6,"errEmailExportMissing":7,"errNothingToImport":8,"errEmailImportFailed":12,"errDraftImportFailed":13,"errDraftLabelFailed":14,"errEncryptMessageAttachment":15,"errEncryptMessage":16,"errNoInternetWhileImport":17,"errUnlockUser":5,"errSourceMessageNotSelected":19,"errCannotParseMail":5000,"errWrongLoginOrPassword":5001,"errWrongServerPathOrPort":5002,"errWrongAuthMethod":5003,"errIMAPFetchFailed":5004,"errLocalSourceLoadFailed":1000,"errPMLoadFailed":1001,"errRemoteSourceLoadFailed":1002,"errLoadAccountList":1005,"errExit":1006,"errRetry":1007,"errAsk":1008,"errImportFailed":1009,"errCreateLabelFailed":1010,"errCreateFolderFailed":1011,"errUpdateLabelFailed":1012,"errUpdateFolderFailed":1013,"errFillFolderName":1014,"errSelectFolderColor":1015,"errNoInternet":1016,"folderTypeSystem":"system","folderTypeLabel":"label","folderTypeFolder":"folder","folderTypeExternal":"external","progressInit":"init","progressLooping":"looping","statusNoInternet":"noInternet","statusCheckingInternet":"internetCheck","statusNewVersionAvailable":"oldVersion","statusUpToDate":"upToDate","statusForceUpdate":"forceupdate"}')
IEStyle{}
MainWindow {
id: winMain
visible : true
Component.onCompleted: {
winMain.showAndRise()
}
}
BugReportWindow {
id:bugreportWin
clientVersion.visible: false
onPrefill : {
userAddress.text=""
if (accountsModel.count>0) {
var addressList = accountsModel.get(0).aliases.split(";")
if (addressList.length>0) {
userAddress.text = addressList[0]
}
}
}
}
// Signals from Go
Connections {
target: go
onProcessFinished : {
winMain.dialogAddUser.hide()
winMain.dialogGlobal.hide()
}
onOpenManual : Qt.openUrlExternally("https://protonmail.com/support/categories/import-export/")
onNotifyBubble : {
//go.highlightSystray()
winMain.bubleNote.text = message
winMain.bubleNote.place(tabIndex)
winMain.bubleNote.show()
winMain.showAndRise()
}
onBubbleClosed : {
if (winMain.updateState=="uptodate") {
//go.normalSystray()
}
}
onSetConnectionStatus: {
go.isConnectionOK = isAvailable
if (go.isConnectionOK) {
if( winMain.updateState==gui.enums.statusNoInternet) {
go.setUpdateState(gui.enums.statusUpToDate)
}
} else {
go.setUpdateState(gui.enums.statusNoInternet)
}
}
onRunCheckVersion : {
go.setUpdateState(gui.enums.statusUpToDate)
winMain.dialogGlobal.state=gui.enums.statusCheckingInternet
winMain.dialogGlobal.show()
go.isNewVersionAvailable(showMessage)
}
onSetUpdateState : {
// once app is outdated prevent from state change
if (winMain.updateState != gui.enums.statusForceUpdate) {
winMain.updateState = updateState
}
}
onSetAddAccountWarning : winMain.dialogAddUser.setWarning(message, 0)
onNotifyVersionIsTheLatest : {
winMain.popupMessage.show(
qsTr("You have the latest version!", "todo")
)
}
onNotifyError : {
var sep = go.errorDescription.indexOf("\n") < 0 ? go.errorDescription.length : go.errorDescription.indexOf("\n")
var name = go.errorDescription.slice(0, sep)
var errorMessage = go.errorDescription.slice(sep)
switch (errCode) {
case gui.enums.errPMLoadFailed :
winMain.popupMessage.show ( qsTr ( "Loading ProtonMail folders and labels was not successful." , "Error message" ) )
winMain.dialogExport.hide()
break
case gui.enums.errLocalSourceLoadFailed :
winMain.popupMessage.show(qsTr(
"Loading local folder structure was not successful. "+
"Folder does not contain valid MBOX or EML file.",
"Error message when can not find correct files in folder."
))
winMain.dialogImport.hide()
break
case gui.enums.errRemoteSourceLoadFailed :
winMain.popupMessage.show ( qsTr ( "Loading remote source structure was not successful." , "Error message" ) )
winMain.dialogImport.hide()
break
case gui.enums.errWrongServerPathOrPort :
winMain.popupMessage.show ( qsTr ( "Cannot contact server - incorrect server address and port." , "Error message" ) )
winMain.dialogImport.decrementCurrentIndex()
break
case gui.enums.errWrongLoginOrPassword :
winMain.popupMessage.show ( qsTr ( "Cannot authenticate - Incorrect email or password." , "Error message" ) )
winMain.dialogImport.decrementCurrentIndex()
break ;
case gui.enums.errWrongAuthMethod :
winMain.popupMessage.show ( qsTr ( "Cannot authenticate - Please use secured authentication method." , "Error message" ) )
winMain.dialogImport.decrementCurrentIndex()
break ;
case gui.enums.errFillFolderName:
winMain.popupMessage.show(qsTr (
"Please fill the name.",
"Error message when user did not fill the name of folder or label"
))
break
case gui.enums.errCreateLabelFailed:
winMain.popupMessage.show(qsTr(
"Cannot create label with name \"%1\"\n%2",
"Error message when it is not possible to create new label, arg1 folder name, arg2 error reason"
).arg(name).arg(errorMessage))
break
case gui.enums.errCreateFolderFailed:
winMain.popupMessage.show(qsTr(
"Cannot create folder with name \"%1\"\n%2",
"Error message when it is not possible to create new folder, arg1 folder name, arg2 error reason"
).arg(name).arg(errorMessage))
break
case gui.enums.errNothingToImport:
winMain.popupMessage.show ( qsTr ( "No emails left to import after date range applied. Please, change the date range to continue." , "Error message" ) )
winMain.dialogImport.decrementCurrentIndex()
break
case gui.enums.errNoInternetWhileImport:
case gui.enums.errNoInternet:
go.setConnectionStatus(false)
winMain.popupMessage.show ( go.canNotReachAPI )
break
case gui.enums.errPMAPIMessageTooLarge:
case gui.enums.errIMAPFetchFailed:
case gui.enums.errEmailImportFailed :
case gui.enums.errDraftImportFailed :
case gui.enums.errDraftLabelFailed :
case gui.enums.errEncryptMessageAttachment:
case gui.enums.errEncryptMessage:
//winMain.dialogImport.ask_retry_skip_cancel(name, errorMessage)
console.log("Import error", errCode, go.errorDescription)
winMain.popupMessage.show(qsTr("Error during import: \n%1\n please see log files for more details.", "message of generic error").arg(go.errorDescription))
winMain.dialogImport.hide()
break;
case gui.enums.errUnknownError : default:
console.log("Unknown Error", errCode, go.errorDescription)
winMain.popupMessage.show(qsTr("The program encounter an unknown error \n%1\n please see log files for more details.", "message of generic error").arg(go.errorDescription))
winMain.dialogExport.hide()
winMain.dialogImport.hide()
winMain.dialogAddUser.hide()
winMain.dialogGlobal.hide()
}
}
onNotifyUpdate : {
go.setUpdateState("forceUpdate")
if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
go.runCheckVersion(false)
winMain.dialogUpdate.show()
}
}
onNotifyLogout : {
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Import-Export app with this account.").arg(accname) )
}
onNotifyAddressChanged : {
go.notifyBubble(0, qsTr("The address list has been changed for account %1. You may need to reconfigure the settings in your email client.").arg(accname) )
}
onNotifyAddressChangedLogout : {
go.notifyBubble(0, qsTr("The address list has been changed for account %1. You have to reconfigure the settings in your email client.").arg(accname) )
}
onNotifyKeychainRebuild : {
go.notifyBubble(1, qsTr(
"Your MacOS keychain is probably corrupted. Please consult the instructions in our <a href=\"https://protonmail.com/bridge/faq#c15\">FAQ</a>.",
"notification message"
))
}
onNotifyHasNoKeychain : {
gui.winMain.dialogGlobal.state="noKeychain"
gui.winMain.dialogGlobal.show()
}
onExportStructureLoadFinished: {
if (okay) winMain.dialogExport.okay()
else winMain.dialogExport.cancel()
}
onImportStructuresLoadFinished: {
if (okay) winMain.dialogImport.okay()
else winMain.dialogImport.cancel()
}
onSimpleErrorHappen: {
if (winMain.dialogImport.visible == true) {
winMain.dialogImport.hide()
}
if (winMain.dialogExport.visible == true) {
winMain.dialogExport.hide()
}
}
}
function folderIcon(folderName, folderType) { // translations
switch (folderName.toLowerCase()) {
case "inbox" : return Style.fa.inbox
case "sent" : return Style.fa.send
case "spam" :
case "junk" : return Style.fa.ban
case "draft" : return Style.fa.file_o
case "starred" : return Style.fa.star_o
case "trash" : return Style.fa.trash_o
case "archive" : return Style.fa.archive
default: return folderType == gui.enums.folderTypeLabel ? Style.fa.tag : Style.fa.folder_open
}
return Style.fa.sticky_note_o
}
function folderTypeTitle(folderType) { // translations
if (folderType==gui.enums.folderTypeSystem ) return ""
if (folderType==gui.enums.folderTypeLabel ) return qsTr("Labels" , "todo")
if (folderType==gui.enums.folderTypeFolder ) return qsTr("Folders" , "todo")
return "Undef"
}
function isFolderEmpty() {
return "true"
}
function getUnixTime(dateString) {
var d = new Date(dateString)
var n = d.getTime()
if (n != n) return -1
return n
}
function getYearList(minY,maxY) {
var years = new Array()
for (var i=0; i<=maxY-minY;i++) {
years[i] = (maxY-i).toString()
}
//console.log("getYearList:", years)
return years
}
function getMonthList(minM,maxM) {
var months = new Array()
for (var i=0; i<=maxM-minM;i++) {
var iMonth = new Date(1989,(i+minM-1),13)
months[i] = iMonth.toLocaleString(gui.locale, "MMM")
}
//console.log("getMonthList:", months[0], months)
return months
}
function getDayList(minD,maxD) {
var days = new Array()
for (var i=0; i<=maxD-minD;i++) {
days[i] = gui.prependZeros(i+minD,2)
}
return days
}
function prependZeros(num,desiredLength) {
var s = num+""
while (s.length < desiredLength) s="0"+s
return s
}
function daysInMonth(year,month) {
if (typeof(year) !== 'number') {
year = parseInt(year)
}
if (typeof(month) !== 'number') {
month = Date.fromLocaleDateString( gui.locale, "1970-"+month+"-10", "yyyy-MMM-dd").getMonth()+1
}
var maxDays = (new Date(year,month,0)).getDate()
if (isNaN(maxDays)) maxDays = 0
//console.log(" daysInMonth", year, month, maxDays)
return maxDays
}
function niceDateTime() {
var stamp = new Date()
var nice = getMonthList(stamp.getMonth()+1, stamp.getMonth()+1)[0]
nice += "-" + getDayList(stamp.getDate(), stamp.getDate())[0]
nice += "-" + getYearList(stamp.getFullYear(), stamp.getFullYear())[0]
nice += " " + gui.prependZeros(stamp.getHours(),2)
nice += ":" + gui.prependZeros(stamp.getMinutes(),2)
return nice
}
/*
// Debug
Connections {
target: structureExternal
onDataChanged: {
console.log("external data changed")
}
}
// Debug
Connections {
target: structurePM
onSelectedLabelsChanged: console.log("PM sel labels:", structurePM.selectedLabels)
onSelectedFoldersChanged: console.log("PM sel folders:", structurePM.selectedFolders)
onDataChanged: {
console.log("PM data changed")
}
}
*/
Timer {
id: checkVersionTimer
repeat : true
triggeredOnStart: false
interval : Style.main.verCheckRepeatTime
onTriggered : go.runCheckVersion(false)
}
property string areYouSureYouWantToQuit : qsTr("There are incomplete processes - some items are not yet transferred. Do you really want to stop and quit?")
// On start
Component.onCompleted : {
// set spell messages
go.wrongCredentials = qsTr("Incorrect username or password." , "notification", -1)
go.wrongMailboxPassword = qsTr("Incorrect mailbox password." , "notification", -1)
go.canNotReachAPI = qsTr("Cannot contact server, please check your internet connection." , "notification", -1)
go.versionCheckFailed = qsTr("Version check was unsuccessful. Please try again later." , "notification", -1)
go.credentialsNotRemoved = qsTr("Credentials could not be removed." , "notification", -1)
go.bugNotSent = qsTr("Unable to submit bug report." , "notification", -1)
go.bugReportSent = qsTr("Bug report successfully sent." , "notification", -1)
go.runCheckVersion(false)
checkVersionTimer.start()
gui.allMonths = getMonthList(1,12)
gui.allMonthsChanged()
}
}

View File

@ -0,0 +1,432 @@
// 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/>.
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
// NOTE: Keep the Column so the height and width is inherited from content
Column {
id: root
state: status
anchors.left: parent.left
property real row_width: 50 * Style.px
property int row_height: Style.accounts.heightAccount
property var listalias : aliases.split(";")
property int iAccount: index
property real spacingLastButtons: (row_width - exportAccount.anchors.leftMargin -Style.main.rightMargin - exportAccount.width - logoutAccount.width - deleteAccount.width)/2
Accessible.role: go.goos=="windows" ? Accessible.Grouping : Accessible.Row
Accessible.name: qsTr("Account %1, status %2", "Accessible text describing account row with arguments: account name and status (connected/disconnected), resp.").arg(account).arg(statusMark.text)
Accessible.description: Accessible.name
Accessible.ignored: !enabled || !visible
// Main row
Rectangle {
id: mainaccRow
anchors.left: parent.left
width : row_width
height : row_height
state: { return isExpanded ? "expanded" : "collapsed" }
color: Style.main.background
property string actionName : (
isExpanded ?
qsTr("Collapse row for account %2", "Accessible text of button showing additional configuration of account") :
qsTr("Expand row for account %2", "Accessible text of button hiding additional configuration of account")
). arg(account)
// override by other buttons
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked : {
if (root.state=="connected") {
mainaccRow.toggle_accountSettings()
}
}
cursorShape : root.state == "connected" ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true
onEntered: {
if (mainaccRow.state=="collapsed") {
mainaccRow.color = Qt.lighter(Style.main.background,1.1)
}
}
onExited: {
if (mainaccRow.state=="collapsed") {
mainaccRow.color = Style.main.background
}
}
}
// toggle down/up icon
Text {
id: toggleIcon
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
leftMargin : Style.main.leftMargin
}
color: Style.main.text
font {
pointSize : Style.accounts.sizeChevron * Style.pt
family : Style.fontawesome.name
}
text: Style.fa.chevron_down
MouseArea {
anchors.fill: parent
onClicked : {
if (root.state=="connected") {
mainaccRow.toggle_accountSettings()
}
}
cursorShape : root.state == "connected" ? Qt.PointingHandCursor : Qt.ArrowCursor
Accessible.role: Accessible.Button
Accessible.name: mainaccRow.actionName
Accessible.description: mainaccRow.actionName
Accessible.onPressAction : {
if (root.state=="connected") {
mainaccRow.toggle_accountSettings()
}
}
Accessible.ignored: root.state!="connected" || !root.enabled
}
}
// account name
TextMetrics {
id: accountMetrics
font : accountName.font
elide: Qt.ElideMiddle
elideWidth: (
statusMark.anchors.leftMargin
- toggleIcon.anchors.leftMargin
)
text: account
}
Text {
id: accountName
anchors {
verticalCenter : parent.verticalCenter
left : toggleIcon.left
leftMargin : Style.main.leftMargin
}
color: Style.main.text
font {
pointSize : (Style.main.fontSize+2*Style.px) * Style.pt
}
text: accountMetrics.elidedText
}
// status
ClickIconText {
id: statusMark
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : row_width/2
}
text : qsTr("connected", "status of a listed logged-in account")
iconText : Style.fa.circle_o
textColor : Style.main.textGreen
enabled : false
Accessible.ignored: true
}
// export
ClickIconText {
id: exportAccount
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : 5.5*row_width/8
}
text : qsTr("Export All", "todo")
iconText : Style.fa.floppy_o
textBold : true
textColor : Style.main.textBlue
onClicked: {
dialogExport.currentIndex = 0
dialogExport.address = account
dialogExport.show()
}
}
// logout
ClickIconText {
id: logoutAccount
anchors {
verticalCenter : parent.verticalCenter
left : exportAccount.right
leftMargin : root.spacingLastButtons
}
text : qsTr("Log out", "action to log out a connected account")
iconText : Style.fa.power_off
textBold : true
textColor : Style.main.textBlue
}
// remove
ClickIconText {
id: deleteAccount
anchors {
verticalCenter : parent.verticalCenter
left : logoutAccount.right
leftMargin : root.spacingLastButtons
}
text : qsTr("Remove", "deletes an account from the account settings page")
iconText : Style.fa.trash_o
textColor : Style.main.text
onClicked : {
dialogGlobal.input=iAccount
dialogGlobal.state="deleteUser"
dialogGlobal.show()
}
}
// functions
function toggle_accountSettings() {
if (root.state=="connected") {
if (mainaccRow.state=="collapsed" ) {
mainaccRow.state="expanded"
} else {
mainaccRow.state="collapsed"
}
}
}
states: [
State {
name: "collapsed"
PropertyChanges { target : toggleIcon ; text : root.state=="connected" ? Style.fa.chevron_down : " " }
PropertyChanges { target : accountName ; font.bold : false }
PropertyChanges { target : mainaccRow ; color : Style.main.background }
PropertyChanges { target : addressList ; visible : false }
},
State {
name: "expanded"
PropertyChanges { target : toggleIcon ; text : Style.fa.chevron_up }
PropertyChanges { target : accountName ; font.bold : true }
PropertyChanges { target : mainaccRow ; color : Style.accounts.backgroundExpanded }
PropertyChanges { target : addressList ; visible : true }
}
]
}
// List of adresses
Column {
id: addressList
anchors.left : parent.left
width: row_width
visible: false
property alias model : repeaterAddresses.model
Repeater {
id: repeaterAddresses
model: ["one", "two"]
Rectangle {
id: addressRow
anchors {
left : parent.left
right : parent.right
}
height: Style.accounts.heightAddrRow
color: Style.accounts.backgroundExpanded
// iconText level down
Text {
id: levelDown
anchors {
left : parent.left
leftMargin : Style.accounts.leftMarginAddr
verticalCenter : wrapAddr.verticalCenter
}
text : Style.fa.level_up
font.family : Style.fontawesome.name
color : Style.main.textDisabled
rotation : 90
}
Rectangle {
id: wrapAddr
anchors {
top : parent.top
left : levelDown.right
right : parent.right
leftMargin : Style.main.leftMargin
rightMargin : Style.main.rightMargin
}
height: Style.accounts.heightAddr
border {
width : Style.main.border
color : Style.main.line
}
color: Style.accounts.backgroundAddrRow
TextMetrics {
id: addressMetrics
font: address.font
elideWidth: (
wrapAddr.width
- address.anchors.leftMargin
- 2*exportAlias.width
- 3*exportAlias.anchors.rightMargin
)
elide: Qt.ElideMiddle
text: modelData
}
Text {
id: address
anchors {
verticalCenter : parent.verticalCenter
left: parent.left
leftMargin: Style.main.leftMargin
}
font.pointSize : Style.main.fontSize * Style.pt
color: Style.main.text
text: addressMetrics.elidedText
}
// export
ClickIconText {
id: exportAlias
anchors {
verticalCenter: parent.verticalCenter
right: importAlias.left
rightMargin: Style.main.rightMargin
}
text: qsTr("Export", "todo")
iconText: Style.fa.floppy_o
textBold: true
textColor: Style.main.textBlue
onClicked: {
dialogExport.address = listalias[index]
dialogExport.show()
}
}
// import
ClickIconText {
id: importAlias
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
rightMargin: Style.main.rightMargin
}
text: qsTr("Import", "todo")
iconText: Style.fa.upload
textBold: true
textColor: enabled ? Style.main.textBlue : Style.main.textDisabled
onClicked: {
dialogImport.address = listalias[index]
dialogImport.show()
}
}
}
}
}
}
// line
Rectangle {
id: line
color: Style.accounts.line
height: Style.accounts.heightLine
width: root.row_width
}
states: [
State {
name: "connected"
PropertyChanges {
target : addressList
model : listalias
}
PropertyChanges {
target : toggleIcon
color : Style.main.text
}
PropertyChanges {
target : accountName
color : Style.main.text
}
PropertyChanges {
target : statusMark
textColor : Style.main.textGreen
text : qsTr("connected", "status of a listed logged-in account")
iconText : Style.fa.circle
}
PropertyChanges {
target: exportAccount
visible: true
}
PropertyChanges {
target : logoutAccount
text : qsTr("Log out", "action to log out a connected account")
onClicked : {
mainaccRow.state="collapsed"
dialogGlobal.state = "logout"
dialogGlobal.input = root.iAccount
dialogGlobal.show()
dialogGlobal.confirmed()
}
}
},
State {
name: "disconnected"
PropertyChanges {
target : addressList
model : 0
}
PropertyChanges {
target : toggleIcon
color : Style.main.textDisabled
}
PropertyChanges {
target : accountName
color : Style.main.textDisabled
}
PropertyChanges {
target : statusMark
textColor : Style.main.textDisabled
text : qsTr("disconnected", "status of a listed logged-out account")
iconText : Style.fa.circle_o
}
PropertyChanges {
target : logoutAccount
text : qsTr("Log in", "action to log in a disconnected account")
onClicked : {
dialogAddUser.username = root.listalias[0]
dialogAddUser.show()
dialogAddUser.inputPassword.focusInput = true
}
}
PropertyChanges {
target: exportAccount
visible: false
}
}
]
}

View File

@ -0,0 +1,51 @@
// 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/>.
// credits
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
Rectangle {
anchors.centerIn: parent
width: Style.main.width
height: root.parent.height - 6*Style.dialog.titleSize
color: "transparent"
ListView {
anchors.fill: parent
clip: true
model: go.credits.split(";")
delegate: AccessibleText {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData
color: Style.main.text
font.pointSize: Style.main.fontSize * Style.pt
}
footer: ButtonRounded {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Close", "close window")
onClicked: dialogCredits.hide()
}
}
}
}

View File

@ -0,0 +1,220 @@
// 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/>.
// input for year / month / day
import QtQuick 2.8
import QtQuick.Controls 2.2
import QtQml.Models 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
ComboBox {
id: root
property string placeholderText : "none"
property var dropDownStyle : Style.dropDownLight
property real radius : Style.dialog.radiusButton
property bool below : true
onDownChanged : {
root.below = popup.y>0
}
font.pointSize : Style.main.fontSize * Style.pt
spacing : Style.dialog.spacing
height : Style.dialog.heightInput
width : 10*Style.px
function updateWidth() {
// make the width according to localization ( especially for Months)
var max = 10*Style.px
if (root.model === undefined) return
for (var i=-1; i<root.model.length; ++i){
metrics.text = i<0 ? root.placeholderText : root.model[i]+"MM" // "M" for extra space
max = Math.max(max, metrics.width)
}
root.width = root.spacing + max + root.spacing + indicatorIcon.width + root.spacing
//console.log("width updated", root.placeholderText, root.width)
}
TextMetrics {
id: metrics
font: root.font
text: placeholderText
}
indicator: Text {
id: indicatorIcon
color: root.enabled ? dropDownStyle.highlight : dropDownStyle.inactive
text: root.down ? Style.fa.chevron_up : Style.fa.chevron_down
font.family: Style.fontawesome.name
anchors {
right: parent.right
rightMargin: root.spacing
verticalCenter: parent.verticalCenter
}
}
contentItem: Text {
id: boxItem
leftPadding: root.spacing
rightPadding: root.spacing
text : enabled && root.currentIndex>=0 ? root.displayText : placeholderText
font : root.font
color : root.enabled ? dropDownStyle.text : dropDownStyle.inactive
verticalAlignment : Text.AlignVCenter
elide : Text.ElideRight
}
background: Rectangle {
color: Style.transparent
MouseArea {
anchors.fill: parent
onClicked: root.down ? root.popup.close() : root.popup.open()
}
}
DelegateModel { // FIXME QML DelegateModel: Error creating delegate
id: filteredData
model: root.model
filterOnGroup: "filtered"
groups: DelegateModelGroup {
id: filtered
name: "filtered"
includeByDefault: true
}
delegate: root.delegate
}
function filterItems(minIndex,maxIndex) {
// filter
var rowCount = filteredData.items.count
if (rowCount<=0) return
//console.log(" filter", root.placeholderText, rowCount, minIndex, maxIndex)
for (var iItem = 0; iItem < rowCount; iItem++) {
var entry = filteredData.items.get(iItem);
entry.inFiltered = ( iItem >= minIndex && iItem <= maxIndex )
//console.log(" inserted ", iItem, rowCount, entry.model.modelData, entry.inFiltered )
}
}
delegate: ItemDelegate {
id: thisItem
width : view.width
height : Style.dialog.heightInput
leftPadding : root.spacing
rightPadding : root.spacing
topPadding : 0
bottomPadding : 0
property int index : {
//console.log( "index: ", thisItem.DelegateModel.itemsIndex )
return thisItem.DelegateModel.itemsIndex
}
onClicked : {
//console.log("thisItem click", thisItem.index)
root.currentIndex = thisItem.index
root.activated(thisItem.index)
root.popup.close()
}
contentItem: Text {
text: modelData
color: dropDownStyle.text
font: root.font
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: thisItem.hovered ? dropDownStyle.highlight : dropDownStyle.background
Text {
anchors{
right: parent.right
rightMargin: root.spacing
verticalCenter: parent.verticalCenter
}
font {
family: Style.fontawesome.name
}
text: root.currentIndex == thisItem.index ? Style.fa.check : ""
color: thisItem.hovered ? dropDownStyle.text : dropDownStyle.highlight
}
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Style.dialog.borderInput
color: dropDownStyle.separator
}
}
}
popup: Popup {
y: root.height
x: -background.strokeWidth
width: root.width + 2*background.strokeWidth
modal: true
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
topPadding: background.radiusTopLeft + 2*background.strokeWidth
bottomPadding: background.radiusBottomLeft + 2*background.strokeWidth
leftPadding: 2*background.strokeWidth
rightPadding: 2*background.strokeWidth
contentItem: ListView {
id: view
clip: true
implicitHeight: winMain.height/3
model: filteredData // if you want to slide down to position: popup.visible ? root.delegateModel : null
currentIndex: root.currentIndex
ScrollIndicator.vertical: ScrollIndicator { }
}
background: RoundedRectangle {
radiusTopLeft : root.below ? 0 : root.radius
radiusBottomLeft : !root.below ? 0 : root.radius
radiusTopRight : radiusTopLeft
radiusBottomRight : radiusBottomLeft
fillColor : dropDownStyle.background
}
}
Component.onCompleted: {
//console.log(" box ", label)
root.updateWidth()
root.filterItems(0,model.length-1)
}
onModelChanged :{
//console.log("model changed", root.placeholderText)
root.updateWidth()
root.filterItems(0,model.length-1)
}
}

View File

@ -0,0 +1,264 @@
// 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/>.
// input for date
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
width : row.width + (root.label == "" ? 0 : textlabel.width)
height : row.height
color : Style.transparent
property alias label : textlabel.text
property string metricsLabel : root.label
property var dropDownStyle : Style.dropDownLight
// dates
property date currentDate : new Date() // default now
property date minDate : new Date(0) // default epoch start
property date maxDate : new Date() // default now
property bool isMaxDateToday : false
property int unix : Math.floor(currentDate.getTime()/1000)
onMinDateChanged: {
if (isNaN(minDate.getTime()) || minDate.getTime() > maxDate.getTime()) {
minDate = new Date(0)
}
//console.log(" minDate changed:", root.label, minDate.toDateString())
updateRange()
}
onMaxDateChanged: {
if (isNaN(maxDate.getTime()) || minDate.getTime() > maxDate.getTime()) {
maxDate = new Date()
}
//console.log(" maxDate changed:", root.label, maxDate.toDateString())
updateRange()
}
RoundedRectangle {
id: background
anchors.fill : row
strokeColor : dropDownStyle.line
strokeWidth : Style.dialog.borderInput
fillColor : dropDownStyle.background
radiusTopLeft : row.children[0].down && !row.children[0].below ? 0 : Style.dialog.radiusButton
radiusBottomLeft : row.children[0].down && row.children[0].below ? 0 : Style.dialog.radiusButton
radiusTopRight : row.children[row.children.length-1].down && !row.children[row.children.length-1].below ? 0 : Style.dialog.radiusButton
radiusBottomRight : row.children[row.children.length-1].down && row.children[row.children.length-1].below ? 0 : Style.dialog.radiusButton
}
TextMetrics {
id: textMetrics
text: root.metricsLabel+"M"
font: textlabel.font
}
Text {
id: textlabel
anchors {
left : root.left
verticalCenter : root.verticalCenter
}
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: dropDownStyle.labelBold
}
color: dropDownStyle.text
width: textMetrics.width
verticalAlignment: Text.AlignVCenter
}
Row {
id: row
anchors {
left : root.label=="" ? root.left : textlabel.right
bottom : root.bottom
}
padding : Style.dialog.borderInput
DateBox {
id: monthInput
placeholderText: qsTr("Month")
enabled: !allDates
model: gui.allMonths
onActivated: updateRange()
anchors.verticalCenter: parent.verticalCenter
dropDownStyle: root.dropDownStyle
onDownChanged: {
if (root.isMaxDateToday){
root.maxDate = new Date()
}
}
}
Rectangle {
width: Style.dialog.borderInput
height: monthInput.height
color: dropDownStyle.line
anchors.verticalCenter: parent.verticalCenter
}
DateBox {
id: dayInput
placeholderText: qsTr("Day")
enabled: !allDates
model: gui.allDays
onActivated: updateRange()
anchors.verticalCenter: parent.verticalCenter
dropDownStyle: root.dropDownStyle
onDownChanged: {
if (root.isMaxDateToday){
root.maxDate = new Date()
}
}
}
Rectangle {
width: Style.dialog.borderInput
height: monthInput.height
color: dropDownStyle.line
}
DateBox {
id: yearInput
placeholderText: qsTr("Year")
enabled: !allDates
model: gui.allYears
onActivated: updateRange()
anchors.verticalCenter: parent.verticalCenter
dropDownStyle: root.dropDownStyle
onDownChanged: {
if (root.isMaxDateToday){
root.maxDate = new Date()
}
}
}
}
function setDate(d) {
//console.trace()
//console.log( " setDate ", label, d)
if (isNaN(d = parseInt(d))) return
var newUnix = Math.min(maxDate.getTime(), d*1000) // seconds to ms
newUnix = Math.max(minDate.getTime(), newUnix)
root.updateRange(new Date(newUnix))
//console.log( " set ", currentDate.getTime())
}
function updateRange(curr) {
if (curr === undefined || isNaN(curr.getTime())) curr = root.getCurrentDate()
//console.log( " update", label, curr, curr.getTime())
//console.trace()
if (isNaN(curr.getTime())) return // shouldn't happen
// full system date range
var firstYear = parseInt(gui.allYears[0])
var firstDay = parseInt(gui.allDays[0])
if ( isNaN(firstYear) || isNaN(firstDay) ) return
// get minimal and maximal available year, month, day
// NOTE: The order is important!!!
var minYear = minDate.getFullYear()
var maxYear = maxDate.getFullYear()
var minMonth = (curr.getFullYear() == minYear ? minDate.getMonth() : 0 )
var maxMonth = (curr.getFullYear() == maxYear ? maxDate.getMonth() : 11 )
var minDay = (
curr.getFullYear() == minYear &&
curr.getMonth() == minMonth ?
minDate.getDate() : firstDay
)
var maxDay = (
curr.getFullYear() == maxYear &&
curr.getMonth() == maxMonth ?
maxDate.getDate() : gui.daysInMonth(curr.getFullYear(), curr.getMonth()+1)
)
//console.log("update ranges: ", root.label, minYear, maxYear, minMonth+1, maxMonth+1, minDay, maxDay)
//console.log("update indexes: ", root.label, firstYear-minYear, firstYear-maxYear, minMonth, maxMonth, minDay-firstDay, maxDay-firstDay)
yearInput.filterItems(firstYear-maxYear, firstYear-minYear)
monthInput.filterItems(minMonth,maxMonth) // getMonth() is index not a month (i.e. Jan==0)
dayInput.filterItems(minDay-1,maxDay-1)
// keep ordering from model not from filter
yearInput .currentIndex = firstYear - curr.getFullYear()
monthInput .currentIndex = curr.getMonth() // getMonth() is index not a month (i.e. Jan==0)
dayInput .currentIndex = curr.getDate()-firstDay
/*
console.log(
"update current indexes: ", root.label,
curr.getFullYear() , '->' , yearInput.currentIndex ,
gui.allMonths[curr.getMonth()] , '->' , monthInput.currentIndex ,
curr.getDate() , '->' , dayInput.currentIndex
)
*/
// test if current date changed
if (
yearInput.currentText == root.currentDate.getFullYear() &&
monthInput.currentText == root.currentDate.toLocaleString(gui.locale, "MMM") &&
dayInput.currentText == gui.prependZeros(root.currentDate.getDate(),2)
) {
//console.log(" currentDate NOT changed", label, root.currentDate.toDateString())
return
}
root.currentDate = root.getCurrentDate()
// console.log(" currentDate changed", label, root.currentDate.toDateString())
}
// get current date from selected
function getCurrentDate() {
if (isNaN(root.currentDate.getTime())) { // wrong current ?
console.log("!WARNING! Wrong current date format", root.currentDate)
root.currentDate = new Date(0)
}
var currentString = ""
var currentUnix = root.currentDate.getTime()
if (
yearInput.currentText != "" &&
yearInput.currentText != yearInput.placeholderText &&
monthInput.currentText != "" &&
monthInput.currentText != monthInput.placeholderText
) {
var day = gui.daysInMonth(yearInput.currentText, monthInput.currentText)
if (!isNaN(parseInt(dayInput.currentText))) {
day = Math.min(day, parseInt(dayInput.currentText))
}
var month = gui.allMonths.indexOf(monthInput.currentText)
var year = parseInt(yearInput.currentText)
var pickedDate = new Date(year, month, day)
// Compensate automatic DST in windows
if (pickedDate.getDate() != day) {
pickedDate.setTime(pickedDate.getTime() + 60*60*1000) // add hour
}
currentUnix = pickedDate.getTime()
}
return new Date(Math.max(
minDate.getTime(),
Math.min(maxDate.getTime(), currentUnix)
))
}
}

View File

@ -0,0 +1,123 @@
// 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/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Column {
id: dateRange
property var structure : transferRules
property string sourceID : "-1"
property alias allDates : allDatesBox.checked
property alias inputDateFrom : inputDateFrom
property alias inputDateTo : inputDateTo
function getRange() {common.getRange()}
function setRangeFromTo(from, to) {common.setRangeFromTo(from, to)}
function applyRange() {common.applyRange()}
property var dropDownStyle : Style.dropDownLight
property var isDark : dropDownStyle.background == Style.dropDownDark.background
spacing: Style.dialog.spacing
DateRangeFunctions {id:common}
DateInput {
id: inputDateFrom
label: qsTr("From:")
currentDate: gui.netBday
maxDate: inputDateTo.currentDate
dropDownStyle: dateRange.dropDownStyle
}
Rectangle {
width: inputDateTo.width
height: Style.dialog.borderInput / 2
color: isDark ? dropDownStyle.separator : Style.transparent
}
DateInput {
id: inputDateTo
label: qsTr("To:")
metricsLabel: inputDateFrom.label
currentDate: new Date() // now
minDate: inputDateFrom.currentDate
isMaxDateToday: true
dropDownStyle: dateRange.dropDownStyle
}
Rectangle {
width: inputDateTo.width
height: Style.dialog.borderInput
color: isDark ? dropDownStyle.separator : Style.transparent
}
CheckBoxLabel {
id: allDatesBox
text : qsTr("All dates")
anchors.right : inputDateTo.right
checkedSymbol : Style.fa.toggle_on
uncheckedSymbol : Style.fa.toggle_off
uncheckedColor : Style.main.textDisabled
textColor : dropDownStyle.text
symbolPointSize : Style.dialog.iconSize * Style.pt * 1.1
spacing : Style.dialog.spacing*2
TextMetrics {
id: metrics
text: allDatesBox.checkedSymbol
font {
family: Style.fontawesome.name
pointSize: allDatesBox.symbolPointSize
}
}
Rectangle {
color: allDatesBox.checked ? dotBackground.color : Style.exporting.sliderBackground
width: metrics.width*0.9
height: metrics.height*0.6
radius: height/2
z: -1
anchors {
left: allDatesBox.left
verticalCenter: allDatesBox.verticalCenter
leftMargin: 0.05 * metrics.width
}
Rectangle {
id: dotBackground
color : Style.exporting.background
height : parent.height
width : height
radius : height/2
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
}
}
}
}
}

View File

@ -0,0 +1,81 @@
// 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/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
/*
NOTE: need to be in obejct with
id: dateRange
property var structure : structureExternal
property string sourceID : structureExternal.getID ( -1 )
property alias allDates : allDatesBox.checked
property alias inputDateFrom : inputDateFrom
property alias inputDateTo : inputDateTo
function getRange() {common.getRange()}
function applyRange() {common.applyRange()}
*/
function resetRange() {
inputDateFrom.setDate(gui.netBday.getTime())
inputDateTo.setDate((new Date()).getTime())
}
function setRangeFromTo(folderFrom, folderTo){ // unix time in seconds
if ( folderFrom == 0 && folderTo ==0 ) {
dateRange.allDates = true
} else {
dateRange.allDates = false
inputDateFrom.setDate(folderFrom)
inputDateTo.setDate(folderTo)
}
}
function getRange(){ // unix time in seconds
//console.log(" ==== GET RANGE === ")
//console.trace()
var folderFrom = dateRange.structure.globalFromDate
var folderTo = dateRange.structure.globalToDate
root.setRangeFromTo(folderFrom, folderTo)
}
function applyRange(){ // unix time is seconds
if (dateRange.allDates) structure.setFromToDate(dateRange.sourceID, 0, 0)
else {
var endOfDay = new Date(inputDateTo.unix*1000)
endOfDay.setHours(23,59,59,999)
var endOfDayUnix = parseInt(endOfDay.getTime()/1000)
structure.setFromToDate(dateRange.sourceID, inputDateFrom.unix, endOfDayUnix)
}
}
Component.onCompleted: {
inputDateFrom.updateRange(gui.netBday)
inputDateTo.updateRange(new Date())
//getRange()
}
}

View File

@ -0,0 +1,163 @@
// 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/>.
// List of import folder and their target
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id:root
width : icon.width + indicator.width + 3*padding
height : icon.height + 3*padding
property real padding : Style.dialog.spacing
property bool down : popup.visible
property var structure : transferRules
property string sourceID : ""
property int sourceFromDate : 0
property int sourceToDate : 0
color: Style.transparent
RoundedRectangle {
anchors.fill: parent
radiusTopLeft: root.down ? 0 : Style.dialog.radiusButton
fillColor: root.down ? Style.main.textBlue : Style.transparent
}
Text {
id: icon
text: Style.fa.calendar_o
anchors {
left : parent.left
leftMargin : root.padding
verticalCenter : parent.verticalCenter
}
color: root.enabled ? (
root.down ? Style.main.background : Style.main.text
) : Style.main.textDisabled
font.family : Style.fontawesome.name
Text {
anchors {
verticalCenter: parent.bottom
horizontalCenter: parent.right
}
color : !root.down && root.enabled ? Style.main.textRed : icon.color
text : Style.fa.exclamation_circle
visible : !dateRangeInput.allDates
font.pointSize : root.padding * Style.pt * 1.5
font.family : Style.fontawesome.name
}
}
Text {
id: indicator
anchors {
right : parent.right
rightMargin : root.padding
verticalCenter : parent.verticalCenter
}
text : root.down ? Style.fa.chevron_up : Style.fa.chevron_down
color : !root.down && root.enabled ? Style.main.textBlue : icon.color
font.family : Style.fontawesome.name
}
MouseArea {
anchors.fill: root
onClicked: {
popup.open()
}
}
Popup {
id: popup
x : -width
modal : true
clip : true
topPadding : 0
background: RoundedRectangle {
fillColor : Style.bubble.paneBackground
strokeColor : fillColor
radiusTopRight: 0
RoundedRectangle {
anchors {
left: parent.left
right: parent.right
top: parent.top
}
height: Style.dialog.heightInput
fillColor: Style.dropDownDark.highlight
strokeColor: fillColor
radiusTopRight: 0
radiusBottomLeft: 0
radiusBottomRight: 0
}
}
contentItem : Column {
spacing: Style.dialog.spacing
Text {
anchors {
left: parent.left
}
text : qsTr("Import date range")
font.bold : Style.dropDownDark.labelBold
color : Style.dropDownDark.text
height : Style.dialog.heightInput
verticalAlignment : Text.AlignVCenter
}
DateRange {
id: dateRangeInput
allDates: true
structure: root.structure
sourceID: root.sourceID
dropDownStyle: Style.dropDownDark
}
}
onAboutToShow : updateRange()
onAboutToHide : dateRangeInput.applyRange()
}
function updateRange() {
dateRangeInput.setRangeFromTo(root.sourceFromDate, root.sourceToDate)
}
Connections {
target:root
onSourceFromDateChanged: root.updateRange()
onSourceToDateChanged: root.updateRange()
}
}

View File

@ -0,0 +1,457 @@
// 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/>.
// Export dialog
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
// TODO
// - make ErrorDialog module
// - map decision to error code : ask (default), skip ()
// - what happens when import fails ? heuristic to find mail where to start from
Dialog {
id: root
enum Page {
LoadingStructure = 0, Options, Progress
}
title : set_title()
property string address
property alias finish: finish
property string msgClearUnfished: qsTr ("Remove already exported files.")
isDialogBusy : true // currentIndex == 0 || currentIndex == 3
signal cancel()
signal okay()
Rectangle { // 0
id: dialogLoading
width: root.width
height: root.height
color: Style.transparent
Text {
anchors.centerIn : dialogLoading
font.pointSize: Style.dialog.titleSize * Style.pt
color: Style.dialog.text
horizontalAlignment: Text.AlignHCenter
text: qsTr("Loading folders and labels for", "todo") +"\n" + address
}
}
Rectangle { // 1
id: dialogInput
width: root.width
height: root.height
color: Style.transparent
Row {
id: inputRow
anchors {
topMargin : root.titleHeight
top : parent.top
horizontalCenter : parent.horizontalCenter
}
spacing: 3*Style.main.leftMargin
property real columnWidth : (root.width - Style.main.leftMargin - inputRow.spacing - Style.main.rightMargin) / 2
property real columnHeight : root.height - root.titleHeight - Style.main.leftMargin
ExportStructure {
id: sourceFoldersInput
width : inputRow.columnWidth
height : inputRow.columnHeight
title : qsTr("From: %1", "todo").arg(address)
}
Column {
spacing: (inputRow.columnHeight - dateRangeInput.height - outputFormatInput.height - outputPathInput.height - buttonRow.height - infotipEncrypted.height) / 4
DateRange{
id: dateRangeInput
}
OutputFormat {
id: outputFormatInput
}
Row {
spacing: Style.dialog.spacing
CheckBoxLabel {
id: exportEncrypted
text: qsTr("Export emails that cannot be decrypted as ciphertext")
anchors {
bottom: parent.bottom
bottomMargin: Style.dialog.fontSize/1.8
}
}
InfoToolTip {
id: infotipEncrypted
anchors {
verticalCenter: exportEncrypted.verticalCenter
}
info: qsTr("Checking this option will export all emails that cannot be decrypted in ciphertext. If this option is not checked, these emails will not be exported", "todo")
}
}
FileAndFolderSelect {
id: outputPathInput
title: qsTr("Select location of export:", "todo")
width : inputRow.columnWidth // stretch folder input
}
Row {
id: buttonRow
anchors.right : parent.right
spacing : Style.dialog.rightMargin
ButtonRounded {
id:buttonCancel
fa_icon: Style.fa.times
text: qsTr("Cancel")
color_main: Style.main.textBlue
onClicked : root.cancel()
}
ButtonRounded {
id: buttonNext
fa_icon: Style.fa.check
text: qsTr("Export","todo")
enabled: transferRules != 0
color_main: Style.dialog.background
color_minor: enabled ? Style.dialog.textBlue : Style.main.textDisabled
isOpaque: true
onClicked : root.okay()
}
}
}
}
}
Rectangle { // 2
id: progressStatus
width: root.width
height: root.height
color: "transparent"
Row {
anchors {
bottom: progressbarExport.top
bottomMargin: Style.dialog.heightSeparator
left: progressbarExport.left
}
spacing: Style.main.rightMargin
AccessibleText {
id: statusLabel
text : qsTr("Status:")
font.pointSize: Style.main.iconSize * Style.pt
color : Style.main.text
}
AccessibleText {
anchors.baseline: statusLabel.baseline
text : {
if (progressbarExport.isFinished) return qsTr("finished")
if (go.progressDescription == "") return qsTr("exporting")
return go.progressDescription
}
elide: Text.ElideMiddle
width: progressbarExport.width - parent.spacing - statusLabel.width
font.pointSize: Style.dialog.textSize * Style.pt
color : Style.main.textDisabled
}
}
ProgressBar {
id: progressbarExport
implicitWidth : 2*progressStatus.width/3
implicitHeight : Style.exporting.rowHeight
value: go.progress
property int current: go.total * go.progress
property bool isFinished: finishedPartBar.width == progressbarExport.width
anchors {
centerIn: parent
}
background: Rectangle {
radius : Style.exporting.boxRadius
color : Style.exporting.progressBackground
}
contentItem: Item {
Rectangle {
id: finishedPartBar
width : parent.width * progressbarExport.visualPosition
height : parent.height
radius : Style.exporting.boxRadius
gradient : Gradient {
GradientStop { position: 0.00; color: Qt.lighter(Style.exporting.progressStatus,1.1) }
GradientStop { position: 0.66; color: Style.exporting.progressStatus }
GradientStop { position: 1.00; color: Qt.darker(Style.exporting.progressStatus,1.1) }
}
Behavior on width {
NumberAnimation { duration:800; easing.type: Easing.InOutQuad }
}
}
Text {
anchors.centerIn: parent
text: {
if (progressbarExport.isFinished) return qsTr("Export finished","todo")
if (
go.progressDescription == gui.enums.progressInit ||
(go.progress==0 && go.description=="")
) {
if (go.total>1) return qsTr("Estimating the total number of messages (%1)","todo").arg(go.total)
else return qsTr("Estimating the total number of messages","todo")
}
var msg = qsTr("Exporting message %1 of %2 (%3%)","todo")
if (pauseButton.paused) msg = qsTr("Exporting paused at message %1 of %2 (%3%)","todo")
return msg.arg(progressbarExport.current).arg(go.total).arg(Math.floor(go.progress*100))
}
color: Style.main.background
font {
pointSize: Style.dialog.fontSize * Style.pt
}
}
}
}
Row {
anchors {
top: progressbarExport.bottom
topMargin : Style.dialog.heightSeparator
horizontalCenter: parent.horizontalCenter
}
spacing: Style.dialog.rightMargin
ButtonRounded {
id: pauseButton
property bool paused : false
fa_icon : paused ? Style.fa.play : Style.fa.pause
text : paused ? qsTr("Resume") : qsTr("Pause")
color_main : Style.dialog.textBlue
onClicked : {
if (paused) {
if (winMain.updateState == gui.enums.statusNoInternet) {
go.notifyError(gui.enums.errNoInternet)
return
}
go.resumeProcess()
} else {
go.pauseProcess()
}
paused = !paused
pauseButton.focus=false
}
visible : !progressbarExport.isFinished
}
ButtonRounded {
fa_icon : Style.fa.times
text : qsTr("Cancel")
color_main : Style.dialog.textBlue
visible : !progressbarExport.isFinished
onClicked : root.ask_cancel_progress()
}
ButtonRounded {
id: finish
fa_icon : Style.fa.check
text : qsTr("Okay","todo")
color_main : Style.dialog.background
color_minor : Style.dialog.textBlue
isOpaque : true
visible : progressbarExport.isFinished
onClicked : root.okay()
}
}
ClickIconText {
id: buttonHelp
anchors {
right : parent.right
bottom : parent.bottom
rightMargin : Style.main.rightMargin
bottomMargin : Style.main.rightMargin
}
textColor : Style.main.textDisabled
iconText : Style.fa.question_circle
text : qsTr("Help", "directs the user to the online user guide")
textBold : true
onClicked : Qt.openUrlExternally("https://protonmail.com/support/categories/import-export/")
}
}
PopupMessage {
id: errorPopup
width: root.width
height: root.height
}
function check_inputs() {
if (currentIndex == 1) {
// at least one email to export
if (transferRules.rowCount() == 0){
errorPopup.show(qsTr("No emails found to export. Please try another address.", "todo"))
return false
}
// at least one source selected
/*
if (!transferRules.atLeastOneSelected) {
errorPopup.show(qsTr("Please select at least one item to export.", "todo"))
return false
}
*/
// check path
var folderCheck = go.checkPathStatus(outputPathInput.path)
switch (folderCheck) {
case gui.enums.pathEmptyPath:
errorPopup.show(qsTr("Missing export path. Please select an output folder."))
break;
case gui.enums.pathWrongPath:
errorPopup.show(qsTr("Folder '%1' not found. Please select an output folder.").arg(outputPathInput.path))
break;
case gui.enums.pathOK | gui.enums.pathNotADir:
errorPopup.show(qsTr("File '%1' is not a folder. Please select an output folder.").arg(outputPathInput.path))
break;
case gui.enums.pathWrongPermissions:
errorPopup.show(qsTr("Cannot access folder '%1'. Please check folder permissions.").arg(outputPathInput.path))
break;
}
if (
(folderCheck&gui.enums.pathOK)==0 ||
(folderCheck&gui.enums.pathNotADir)==gui.enums.pathNotADir
) return false
if (winMain.updateState == gui.enums.statusNoInternet) {
errorPopup.show(qsTr("Please check your internet connection."))
return false
}
}
return true
}
function set_title() {
switch(root.currentIndex){
case 1 : return qsTr("Select what you'd like to export:")
default: return ""
}
}
function clear_status() {
go.progress=0.0
go.total=0.0
go.progressDescription=gui.enums.progressInit
}
function ask_cancel_progress(){
errorPopup.buttonYes.visible = true
errorPopup.buttonNo.visible = true
errorPopup.buttonOkay.visible = false
errorPopup.show ("Are you sure you want to cancel this export?")
}
onCancel : {
switch (root.currentIndex) {
case 0 :
case 1 : root.hide(); break;
case 2 : // progress bar
go.cancelProcess();
// no break
default:
root.clear_status()
root.currentIndex=1
}
}
onOkay : {
var isOK = check_inputs()
if (!isOK) return
timer.interval= currentIndex==1 ? 1 : 300
switch (root.currentIndex) {
case 2: // progress
root.clear_status()
root.hide()
break
case 0: // loading structure
dateRangeInput.getRange()
//no break
default:
incrementCurrentIndex()
timer.start()
}
}
onShow: {
if (winMain.updateState==gui.enums.statusNoInternet) {
go.checkInternet()
if (winMain.updateState==gui.enums.statusNoInternet) {
go.notifyError(gui.enums.errNoInternet)
root.hide()
return
}
}
root.clear_status()
root.currentIndex=0
timer.interval = 300
timer.start()
dateRangeInput.allDates = true
}
Connections {
target: timer
onTriggered : {
switch (currentIndex) {
case 0:
go.loadStructureForExport(root.address)
sourceFoldersInput.hasItems = (transferRules.rowCount() > 0)
break
case 2:
dateRangeInput.applyRange()
go.startExport(
outputPathInput.path,
root.address,
outputFormatInput.checkedText,
exportEncrypted.checked
)
break
}
}
}
Connections {
target: errorPopup
onClickedOkay : errorPopup.hide()
onClickedYes : {
root.cancel()
errorPopup.hide()
}
onClickedNo : {
go.resumeProcess()
errorPopup.hide()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,354 @@
// 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/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Dialog {
id: root
title : ""
property string input
property alias question : msg.text
property alias note : noteText.text
property alias answer : answ.text
property alias buttonYes : buttonYes
property alias buttonNo : buttonNo
isDialogBusy: currentIndex==1
signal confirmed()
Column {
id: dialogMessage
property int heightInputs : msg.height+
middleSep.height+
buttonRow.height +
(checkboxSep.visible ? checkboxSep.height : 0 ) +
(noteSep.visible ? noteSep.height : 0 ) +
(checkBoxWrapper.visible ? checkBoxWrapper.height : 0 ) +
(root.note!="" ? noteText.height : 0 )
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogMessage.heightInputs)/2 }
AccessibleText {
id:noteText
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: false
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Rectangle { id: noteSep; visible: note!=""; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
AccessibleText {
id: msg
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
width: 2*parent.width/3
text : ""
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Rectangle { id: checkboxSep; visible: checkBoxWrapper.visible; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
Row {
id: checkBoxWrapper
property bool isChecked : false
visible: root.state=="deleteUser"
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
function toggle() {
checkBoxWrapper.isChecked = !checkBoxWrapper.isChecked
}
Text {
id: checkbox
font {
pointSize : Style.dialog.iconSize * Style.pt
family : Style.fontawesome.name
}
anchors.verticalCenter : parent.verticalCenter
text: checkBoxWrapper.isChecked ? Style.fa.check_square_o : Style.fa.square_o
color: checkBoxWrapper.isChecked ? Style.main.textBlue : Style.main.text
MouseArea {
anchors.fill: parent
onPressed: checkBoxWrapper.toggle()
cursorShape: Qt.PointingHandCursor
}
}
Text {
id: checkBoxNote
anchors.verticalCenter : parent.verticalCenter
text: qsTr("Additionally delete all stored preferences and data", "when removing an account, this extra preference additionally deletes all cached data")
color: Style.main.text
font.pointSize: Style.dialog.fontSize * Style.pt
MouseArea {
anchors.fill: parent
onPressed: checkBoxWrapper.toggle()
cursorShape: Qt.PointingHandCursor
Accessible.role: Accessible.CheckBox
Accessible.checked: checkBoxWrapper.isChecked
Accessible.name: checkBoxNote.text
Accessible.description: checkBoxNote.text
Accessible.ignored: checkBoxNote.text == ""
Accessible.onToggleAction: checkBoxWrapper.toggle()
Accessible.onPressAction: checkBoxWrapper.toggle()
}
}
}
Rectangle { id: middleSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main : Style.dialog.textBlue
fa_icon : Style.fa.times
text : qsTr("No")
onClicked : root.hide()
}
ButtonRounded {
id: buttonYes
color_main : Style.dialog.background
color_minor : Style.dialog.textBlue
isOpaque : true
fa_icon : Style.fa.check
text : qsTr("Yes")
onClicked : {
currentIndex=1
root.confirmed()
}
}
}
}
Column {
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-answ.height)/2 }
AccessibleText {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
width: 3*parent.width/4
horizontalAlignment: Text.AlignHCenter
text : qsTr("Waiting...", "in general this displays between screens when processing data takes a long time")
wrapMode: Text.Wrap
}
}
states : [
State {
name: "quit"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Close ImportExport", "quits the application")
question : qsTr("Are you sure you want to close the ImportExport?", "asked when user tries to quit the application")
note : ""
answer : qsTr("Closing ImportExport...", "displayed when user is quitting application")
}
},
State {
name: "logout"
PropertyChanges {
target: root
currentIndex : 1
title : qsTr("Logout", "title of page that displays during account logout")
question : ""
note : ""
answer : qsTr("Logging out...", "displays during account logout")
}
},
State {
name: "deleteUser"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Delete account", "title of page that displays during account deletion")
question : qsTr("Are you sure you want to remove this account?", "displays during account deletion")
note : ""
answer : qsTr("Deleting ...", "displays during account deletion")
}
},
State {
name: "clearChain"
PropertyChanges {
target : root
currentIndex : 0
title : qsTr("Clear keychain", "title of page that displays during keychain clearing")
question : qsTr("Are you sure you want to clear your keychain?", "displays during keychain clearing")
note : qsTr("This will remove all accounts that you have added to the Import-Export app.", "displays during keychain clearing")
answer : qsTr("Clearing the keychain ...", "displays during keychain clearing")
}
},
State {
name: "clearCache"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Clear cache", "title of page that displays during cache clearing")
question : qsTr("Are you sure you want to clear your local cache?", "displays during cache clearing")
note : qsTr("This will delete all of your stored preferences.", "displays during cache clearing")
answer : qsTr("Clearing the cache ...", "displays during cache clearing")
}
},
State {
name: "checkUpdates"
PropertyChanges {
target: root
currentIndex : 1
title : ""
question : ""
note : ""
answer : qsTr("Checking for updates ...", "displays if user clicks the Check for Updates button in the Help tab")
}
},
State {
name: "internetCheck"
PropertyChanges {
target: root
currentIndex : 1
title : ""
question : ""
note : ""
answer : qsTr("Contacting server...", "displays if user clicks the Check for Updates button in the Help tab")
}
},
State {
name: "addressmode"
PropertyChanges {
target: root
currentIndex : 0
title : ""
question : qsTr("Do you want to continue?", "asked when the user changes between split and combined address mode")
note : qsTr("Changing between split and combined address mode will require you to delete your account(s) from your email client and begin the setup process from scratch.", "displayed when the user changes between split and combined address mode")
answer : qsTr("Configuring address mode for ", "displayed when the user changes between split and combined address mode") + root.input
}
},
State {
name: "toggleAutoStart"
PropertyChanges {
target: root
currentIndex : 1
question : ""
note : ""
title : ""
answer : {
var msgTurnOn = qsTr("Turning on automatic start of ImportExport...", "when the automatic start feature is selected")
var msgTurnOff = qsTr("Turning off automatic start of ImportExport...", "when the automatic start feature is deselected")
return go.isAutoStart==0 ? msgTurnOff : msgTurnOn
}
}
},
State {
name: "undef";
PropertyChanges {
target: root
currentIndex : 1
question : ""
note : ""
title : ""
answer : ""
}
}
]
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
onHide: {
checkBoxWrapper.isChecked = false
state = "undef"
}
onShow: {
// hide all other dialogs
winMain.dialogAddUser .visible = false
winMain.dialogCredits .visible = false
//winMain.dialogVersionInfo .visible = false
// dialogFirstStart should reappear again after closing global
root.visible = true
}
onConfirmed : {
if (state == "quit" || state == "instance exists" ) {
timer.interval = 1000
} else {
timer.interval = 300
}
answ.forceActiveFocus()
timer.start()
}
Connections {
target: timer
onTriggered: {
if ( state == "addressmode" ) { go.switchAddressMode (input) }
if ( state == "clearChain" ) { go.clearKeychain () }
if ( state == "clearCache" ) { go.clearCache () }
if ( state == "deleteUser" ) { go.deleteAccount (input, checkBoxWrapper.isChecked) }
if ( state == "logout" ) { go.logoutAccount (input) }
if ( state == "toggleAutoStart" ) { go.toggleAutoStart () }
if ( state == "quit" ) { Qt.quit () }
if ( state == "instance exists" ) { Qt.quit () }
if ( state == "checkUpdates" ) { go.runCheckVersion (true) }
}
}
Keys.onPressed: {
if (event.key == Qt.Key_Enter) {
root.confirmed()
}
}
}

View File

@ -0,0 +1,149 @@
// 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/>.
// List of export folders / labels
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
color : Style.exporting.background
radius : Style.exporting.boxRadius
border {
color : Style.exporting.line
width : Style.dialog.borderInput
}
property bool hasItems: true
Text { // placeholder
visible: !root.hasItems
anchors.centerIn: parent
color: Style.main.textDisabled
font {
pointSize: Style.dialog.fontSize * Style.pt
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: qsTr("No emails found for this address.","todo")
}
property string title : ""
TextMetrics {
id: titleMetrics
text: root.title
elide: Qt.ElideMiddle
elideWidth: root.width - 4*Style.exporting.leftMargin
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
}
Rectangle {
id: header
anchors {
top: root.top
left: root.left
}
width : root.width
height : Style.dialog.fontSize*3
color : Style.transparent
Rectangle {
anchors.bottom: parent.bottom
color : Style.exporting.line
height : Style.dialog.borderInput
width : parent.width
}
Text {
anchors {
left : parent.left
leftMargin : 2*Style.exporting.leftMargin
verticalCenter : parent.verticalCenter
}
color: Style.dialog.text
font: titleMetrics.font
text: titleMetrics.elidedText
}
}
ListView {
id: listview
clip : true
orientation : ListView.Vertical
boundsBehavior : Flickable.StopAtBounds
model : transferRules
cacheBuffer : 10000
anchors {
left : root.left
right : root.right
bottom : root.bottom
top : header.bottom
margins : Style.dialog.borderInput
}
ScrollBar.vertical: ScrollBar {
/*
policy: ScrollBar.AsNeeded
background : Rectangle {
color : Style.exporting.sliderBackground
radius : Style.exporting.boxRadius
}
contentItem : Rectangle {
color : Style.exporting.sliderForeground
radius : Style.exporting.boxRadius
implicitWidth : Style.main.rightMargin / 3
}
*/
anchors {
right: parent.right
rightMargin: Style.main.rightMargin/4
}
width: Style.main.rightMargin/3
Accessible.ignored: true
}
delegate: FolderRowButton {
property variant modelData: model
width : root.width - 5*root.border.width
type : modelData.type
folderIconColor : modelData.iconColor
title : modelData.name
isSelected : modelData.isActive
onClicked : {
//console.log("Clicked", folderId, isSelected)
transferRules.setIsRuleActive(modelData.mboxID,!model.isActive)
}
}
section.property: "type"
section.delegate: FolderRowButton {
isSection : true
width : root.width - 5*root.border.width
title : gui.folderTypeTitle(section)
isSelected : section == gui.enums.folderTypeLabel ? transferRules.isLabelGroupSelected : transferRules.isFolderGroupSelected
onClicked : transferRules.setIsGroupActive(section,!isSelected)
}
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Filter only selected folders or labels
import QtQuick 2.8
import QtQml.Models 2.2
DelegateModel {
id: root
model : structurePM
//filterOnGroup : root.folderType
//delegate : root.delegate
groups : [
DelegateModelGroup {name: gui.enums.folderTypeFolder ; includeByDefault: false},
DelegateModelGroup {name: gui.enums.folderTypeLabel ; includeByDefault: false}
]
function updateFilter() {
//console.log("FilterModelDelegate::UpdateFilter")
// filter
var rowCount = root.items.count;
for (var iItem = 0; iItem < rowCount; iItem++) {
var entry = root.items.get(iItem);
entry.inLabel = (
root.filterOnGroup == gui.enums.folderTypeLabel &&
entry.model.folderType == gui.enums.folderTypeLabel
)
entry.inFolder = (
root.filterOnGroup == gui.enums.folderTypeFolder &&
entry.model.folderType != gui.enums.folderTypeLabel
)
/*
if (entry.inFolder && entry.model.folderId == selectedIDs) {
view.currentIndex = iItem
}
*/
//console.log("::::update filter:::::", iItem, entry.model.folderName, entry.inFolder, entry.inLabel)
}
}
}

View File

@ -0,0 +1,99 @@
// 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/>.
// Checkbox row for folder selection
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
AccessibleButton {
id: root
property bool isSection : false
property bool isSelected : false
property string title : "N/A"
property string type : ""
property string folderIconColor : Style.main.textBlue
height : Style.exporting.rowHeight
padding : 0.0
anchors {
horizontalCenter: parent.horizontalCenter
}
background: Rectangle {
color: isSection ? Style.exporting.background : Style.exporting.rowBackground
Rectangle { // line
anchors.bottom : parent.bottom
height : Style.dialog.borderInput
width : parent.width
color : Style.exporting.background
}
}
contentItem: Rectangle {
color: "transparent"
id: content
Text {
id: checkbox
anchors {
verticalCenter : parent.verticalCenter
left : content.left
leftMargin : Style.exporting.leftMargin * (root.type == gui.enums.folderTypeSystem ? 1.0 : 2.0)
}
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
color : isSelected ? Style.main.text : Style.main.textInactive
text : (isSelected ? Style.fa.check_square_o : Style.fa.square_o )
}
Text { // icon
id: folderIcon
visible: !isSection
anchors {
verticalCenter : parent.verticalCenter
left : checkbox.left
leftMargin : Style.dialog.fontSize + Style.exporting.leftMargin
}
color : root.type=="" ? Style.main.textBlue : root.folderIconColor
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
text : {
return gui.folderIcon(root.title.toLowerCase(), root.type)
}
}
Text {
text: root.title
anchors {
verticalCenter : parent.verticalCenter
left : isSection ? checkbox.left : folderIcon.left
leftMargin : Style.dialog.fontSize + Style.exporting.leftMargin
}
font {
pointSize : Style.dialog.fontSize * Style.pt
bold: isSection
}
color: Style.exporting.text
}
}
}

View File

@ -0,0 +1,129 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List the settings
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
// must have wrapper
Rectangle {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
color: Style.main.background
// content
Column {
anchors.horizontalCenter : parent.horizontalCenter
ButtonIconText {
id: manual
anchors.left: parent.left
text: qsTr("Setup Guide")
leftIcon.text : Style.fa.book
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: go.openManual()
}
ButtonIconText {
id: updates
anchors.left: parent.left
text: qsTr("Check for Updates")
leftIcon.text : Style.fa.refresh
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: {
dialogGlobal.state="checkUpdates"
dialogGlobal.show()
dialogGlobal.confirmed()
}
}
Rectangle {
anchors.horizontalCenter : parent.horizontalCenter
height: Math.max (
aboutText.height +
Style.main.fontSize,
wrapper.height - (
2*manual.height +
creditsLink.height +
Style.main.fontSize
)
)
width: wrapper.width
color : Style.transparent
Text {
id: aboutText
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
color: Style.main.textDisabled
horizontalAlignment: Qt.AlignHCenter
font.family : Style.fontawesome.name
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n"+Style.fa.copyright + " 2020 Proton Technologies AG"
}
}
Row {
anchors.horizontalCenter : parent.horizontalCenter
spacing : Style.main.dummy
Text {
id: creditsLink
text : qsTr("Credits", "link to click on to view list of credited libraries")
color : Style.main.textDisabled
font.pointSize: Style.main.fontSize * Style.pt
font.underline: true
MouseArea {
anchors.fill: parent
onClicked : {
winMain.dialogCredits.show()
}
cursorShape: Qt.PointingHandCursor
}
}
Text {
id: releaseNotes
text : qsTr("Release notes", "link to click on to view release notes for this version of the app")
color : Style.main.textDisabled
font.pointSize: Style.main.fontSize * Style.pt
font.underline: true
MouseArea {
anchors.fill: parent
onClicked : {
go.getLocalVersionInfo()
winMain.dialogVersionInfo.show()
}
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
}

View File

@ -0,0 +1,106 @@
// 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/>.
// Adjust Bridge Style
import QtQuick 2.8
import ImportExportUI 1.0
import ProtonUI 1.0
Item {
Component.onCompleted : {
//Style.refdpi = go.goos == "darwin" ? 86.0 : 96.0
Style.pt = go.goos == "darwin" ? 93/Style.dpi : 80/Style.dpi
Style.main.background = "#fff"
Style.main.text = "#505061"
Style.main.textInactive = "#686876"
Style.main.line = "#dddddd"
Style.main.width = 884 * Style.px
Style.main.height = 422 * Style.px
Style.main.leftMargin = 25 * Style.px
Style.main.rightMargin = 25 * Style.px
Style.title.background = Style.main.text
Style.title.text = Style.main.background
Style.tabbar.background = "#3D3A47"
Style.tabbar.rightButton = "add account"
Style.tabbar.spacingButton = 45*Style.px
Style.accounts.backgroundExpanded = "#fafafa"
Style.accounts.backgroundAddrRow = "#fff"
Style.accounts.leftMargin2 = Style.main.width/2
Style.accounts.leftMargin3 = 5.5*Style.main.width/8
Style.dialog.background = "#fff"
Style.dialog.text = Style.main.text
Style.dialog.line = "#e2e2e2"
Style.dialog.fontSize = 12 * Style.px
Style.dialog.heightInput = 2.2*Style.dialog.fontSize
Style.dialog.heightButton = Style.dialog.heightInput
Style.dialog.borderInput = 1 * Style.px
Style.bubble.background = "#595966"
Style.bubble.paneBackground = "#454553"
Style.bubble.text = "#fff"
Style.bubble.width = 310 * Style.px
Style.bubble.widthPane = 36 * Style.px
Style.bubble.iconSize = 14 * Style.px
// colors:
// text: #515061
// tick: #686876
// blue icon: #9396cc
// row bck: #f8f8f9
// line: #ddddde or #e2e2e2
//
// slider bg: #e6e6e6
// slider fg: #515061
// info icon: #c3c3c8
// input border: #ebebeb
//
// bubble color: #595966
// bubble pane: #454553
// bubble text: #fff
//
// indent folder
//
// Dimensions:
// full width: 882px
// leftMargin: 25px
// rightMargin: 25px
// rightMargin: 25px
// middleSeparator: 69px
// width folders: 416px or (width - separators) /2
// width output: 346px or (width - separators) /2
//
// height from top to input begin: 78px
// heightSeparator: 27px
// height folder input: 26px
//
// buble width: 309px
// buble left pane icon: 14px
// buble left pane width: 36px or (2.5 icon width)
// buble height: 46px
// buble arrow height: 12px
// buble arrow width: 14px
// buble radius: 3-4px
}
}

View File

@ -0,0 +1,154 @@
// 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/>.
// List of import folder and their target
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
color: Style.importing.rowBackground
height: 40
width: 300
property real leftMargin1 : folderIcon.x - root.x
property real leftMargin2 : selectFolder.x - root.x
property real nameWidth : {
var available = root.width
available -= rowPlacement.children.length * rowPlacement.spacing // spacing between places
available -= 3*rowPlacement.leftPadding // left, and 2x right
available -= folderIcon.width
available -= arrowIcon.width
available -= dateRangeMenu.width
return available/3.3 // source folder label, target folder menu, target labels menu, and 0.3x label list
}
property real iconWidth : nameWidth*0.3
property bool isSourceSelected: isActive
property string lastTargetFolder: "6" // Archive
property string lastTargetLabels: "" // no flag by default
property string sourceID : mboxID
property string sourceName : name
Rectangle {
id: line
anchors {
left : parent.left
right : parent.right
bottom : parent.bottom
}
height : Style.main.border * 2
color : Style.importing.rowLine
}
Row {
id: rowPlacement
spacing: Style.dialog.spacing
leftPadding: Style.dialog.spacing*2
anchors.verticalCenter : parent.verticalCenter
CheckBoxLabel {
id: checkBox
anchors.verticalCenter : parent.verticalCenter
checked: root.isSourceSelected
onClicked: root.toggleImport()
}
Text {
id: folderIcon
text : gui.folderIcon(root.sourceName, gui.enums.folderTypeFolder)
anchors.verticalCenter : parent.verticalCenter
color: root.isSourceSelected ? Style.main.text : Style.main.textDisabled
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
Text {
text : root.sourceName
width: nameWidth
elide: Text.ElideRight
anchors.verticalCenter : parent.verticalCenter
color: folderIcon.color
font.pointSize : Style.dialog.fontSize * Style.pt
}
Text {
id: arrowIcon
text : Style.fa.arrow_right
anchors.verticalCenter : parent.verticalCenter
color: Style.main.text
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
SelectFolderMenu {
id: selectFolder
sourceID: root.sourceID
targets: transferRules.targetFolders(root.sourceID)
width: nameWidth
anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
onDoNotImport: root.toggleImport()
onImportToFolder: root.importToFolder(newTargetID)
}
SelectLabelsMenu {
sourceID: root.sourceID
targets: transferRules.targetLabels(root.sourceID)
width: nameWidth
anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
onAddTargetLabel: { transferRules.addTargetID(sourceID, newTargetID) }
onRemoveTargetLabel: { transferRules.removeTargetID(sourceID, newTargetID) }
}
LabelIconList {
colorList: labelColors=="" ? [] : labelColors.split(";")
width: iconWidth
anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
}
DateRangeMenu {
id: dateRangeMenu
sourceID: root.sourceID
sourceFromDate: fromDate
sourceToDate: toDate
enabled: root.isSourceSelected
anchors.verticalCenter : parent.verticalCenter
Component.onCompleted : dateRangeMenu.updateRange()
}
}
function importToFolder(newTargetID) {
transferRules.addTargetID(root.sourceID,newTargetID)
}
function toggleImport() {
transferRules.setIsRuleActive(root.sourceID, !root.isSourceSelected)
}
}

View File

@ -0,0 +1,216 @@
// 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/>.
// Import report modal
import QtQuick 2.11
import QtQuick.Controls 2.4
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
color: "#aa101021"
visible: false
MouseArea { // disable bellow
anchors.fill: root
hoverEnabled: true
}
Rectangle {
id:background
color: Style.main.background
anchors {
fill : root
topMargin : Style.main.rightMargin
leftMargin : 2*Style.main.rightMargin
rightMargin : 2*Style.main.rightMargin
bottomMargin : 2.5*Style.main.rightMargin
}
ClickIconText {
anchors {
top : parent.top
right : parent.right
margins : .5* Style.main.rightMargin
}
iconText : Style.fa.times
text : ""
textColor : Style.main.textBlue
onClicked : root.hide()
Accessible.description : qsTr("Close dialog %1", "Click to exit modal.").arg(title.text)
}
Text {
id: title
text : qsTr("List of errors")
font {
pointSize: Style.dialog.titleSize * Style.pt
}
anchors {
top : parent.top
topMargin : 0.5*Style.main.rightMargin
horizontalCenter : parent.horizontalCenter
}
}
ListView {
id: errorView
anchors {
left : parent.left
right : parent.right
top : title.bottom
bottom : detailBtn.top
margins : Style.main.rightMargin
}
clip : true
flickableDirection : Flickable.HorizontalAndVerticalFlick
contentWidth : errorView.rWall
boundsBehavior : Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
anchors {
right : parent.right
top : parent.top
rightMargin : Style.main.rightMargin/4
topMargin : Style.main.rightMargin
}
width: Style.main.rightMargin/3
Accessible.ignored: true
}
ScrollBar.horizontal: ScrollBar {
anchors {
bottom : parent.bottom
right : parent.right
bottomMargin : Style.main.rightMargin/4
rightMargin : Style.main.rightMargin
}
height: Style.main.rightMargin/3
Accessible.ignored: true
}
property real rW1 : 150 *Style.px
property real rW2 : 150 *Style.px
property real rW3 : 100 *Style.px
property real rW4 : 150 *Style.px
property real rW5 : 550 *Style.px
property real rWall : errorView.rW1+errorView.rW2+errorView.rW3+errorView.rW4+errorView.rW5
property real pH : .5*Style.main.rightMargin
model : errorList
delegate : Rectangle {
width : Math.max(errorView.width, row.width)
height : row.height
Row {
id: row
spacing : errorView.pH
leftPadding : errorView.pH
rightPadding : errorView.pH
topPadding : errorView.pH
bottomPadding : errorView.pH
ImportReportCell { width : errorView.rW1; text : mailSubject }
ImportReportCell { width : errorView.rW2; text : mailDate }
ImportReportCell { width : errorView.rW3; text : inputFolder }
ImportReportCell { width : errorView.rW4; text : mailFrom }
ImportReportCell { width : errorView.rW5; text : errorMessage }
}
Rectangle {
color : Style.main.line
height : .8*Style.px
width : parent.width
anchors.left : parent.left
anchors.bottom : parent.bottom
}
}
headerPositioning: ListView.OverlayHeader
header: Rectangle {
height : viewHeader.height
width : Math.max(errorView.width, viewHeader.width)
color : Style.accounts.backgroundExpanded
z : 2
Row {
id: viewHeader
spacing : errorView.pH
leftPadding : errorView.pH
rightPadding : errorView.pH
topPadding : .5*errorView.pH
bottomPadding : .5*errorView.pH
ImportReportCell { width : errorView.rW1 ; text : qsTr ( "SUBJECT" ); isHeader: true }
ImportReportCell { width : errorView.rW2 ; text : qsTr ( "DATE/TIME" ); isHeader: true }
ImportReportCell { width : errorView.rW3 ; text : qsTr ( "FOLDER" ); isHeader: true }
ImportReportCell { width : errorView.rW4 ; text : qsTr ( "FROM" ); isHeader: true }
ImportReportCell { width : errorView.rW5 ; text : qsTr ( "ERROR" ); isHeader: true }
}
Rectangle {
color : Style.main.line
height : .8*Style.px
width : parent.width
anchors.left : parent.left
anchors.bottom : parent.bottom
}
}
}
Rectangle {
anchors{
fill : errorView
margins : -radius
}
radius : 2* Style.px
color : Style.transparent
border {
width : Style.px
color : Style.main.line
}
}
ButtonRounded {
id: detailBtn
fa_icon : Style.fa.file_text
text : qsTr("Detailed file")
color_main : Style.dialog.textBlue
onClicked : go.importLogFileName == "" ? go.openLogs() : go.openReport()
anchors {
bottom : parent.bottom
bottomMargin : 0.5*Style.main.rightMargin
horizontalCenter : parent.horizontalCenter
}
}
}
function show() {
root.visible = true
}
function hide() {
root.visible = false
}
}

View File

@ -0,0 +1,67 @@
// 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/>.
// Import report modal
import QtQuick 2.11
import QtQuick.Controls 2.4
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
property alias text : cellText.text
property bool isHeader : false
property bool isHovered : false
property bool isWider : cellText.contentWidth > root.width
width : 20*Style.px
height : cellText.height
z : root.isHovered ? 3 : 1
color : Style.transparent
Rectangle {
anchors {
fill : cellText
margins : -2*Style.px
}
color : root.isWider ? Style.main.background : Style.transparent
border {
color : root.isWider ? Style.main.textDisabled : Style.transparent
width : Style.px
}
}
Text {
id: cellText
color : root.isHeader ? Style.main.textDisabled : Style.main.text
elide : root.isHovered ? Text.ElideNone : Text.ElideRight
width : root.isHovered ? cellText.contentWidth : root.width
font {
pointSize : Style.main.textSize * Style.pt
family : Style.fontawesome.name
}
}
MouseArea {
anchors.fill : root
hoverEnabled : !root.isHeader
onEntered : { root.isHovered = true }
onExited : { root.isHovered = false }
}
}

View File

@ -0,0 +1,89 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Export dialog
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Button {
id: root
width : 200
height : icon.height + 4*tag.height
scale : pressed ? 0.95 : 1.0
property string iconText : Style.fa.ban
background: Rectangle { color: "transparent" }
contentItem: Rectangle {
id: wrapper
color: "transparent"
Image {
id: icon
anchors {
bottom : wrapper.bottom
bottomMargin : tag.height*2.5
horizontalCenter : wrapper.horizontalCenter
}
fillMode : Image.PreserveAspectFit
width : Style.main.fontSize * 7
mipmap : true
source : "images/"+iconText+".png"
}
Row {
spacing: Style.dialog.spacing
anchors {
bottom : wrapper.bottom
horizontalCenter : wrapper.horizontalCenter
}
Text {
id: tag
text : Style.fa.plus_circle
color : Qt.lighter( Style.dialog.textBlue, root.enabled ? 1.0 : 1.5)
font {
family : Style.fontawesome.name
pointSize : Style.main.fontSize * Style.pt * 1.2
}
}
Text {
text : root.text
color: tag.color
font {
family : tag.font.family
pointSize : tag.font.pointSize
weight : Font.DemiBold
underline : true
}
}
}
}
}

View File

@ -0,0 +1,148 @@
// 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/>.
// List of import folder and their target
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
property string titleFrom
property string titleTo
property bool hasItems: true
color : Style.transparent
Rectangle {
anchors.fill: root
radius : Style.dialog.radiusButton
color : Style.transparent
border {
color : Style.main.line
width : 1.5*Style.dialog.borderInput
}
Text { // placeholder
visible: !root.hasItems
anchors.centerIn: parent
color: Style.main.textDisabled
font {
pointSize: Style.dialog.fontSize * Style.pt
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: qsTr("No emails found for this source.","todo")
}
}
anchors {
left : parent.left
right : parent.right
top : parent.top
bottom : parent.bottom
leftMargin : Style.main.leftMargin
rightMargin : Style.main.leftMargin
topMargin : Style.main.topMargin
bottomMargin : Style.main.bottomMargin
}
ListView {
id: listview
clip : true
orientation : ListView.Vertical
boundsBehavior : Flickable.StopAtBounds
model : transferRules
cacheBuffer : 10000
delegate : ImportDelegate {
width: root.width
}
anchors {
top: titleBox.bottom
bottom: root.bottom
left: root.left
right: root.right
margins : Style.dialog.borderInput
bottomMargin: Style.dialog.radiusButton
}
ScrollBar.vertical: ScrollBar {
anchors {
right: parent.right
rightMargin: Style.main.rightMargin/4
}
width: Style.main.rightMargin/3
Accessible.ignored: true
}
}
Rectangle {
id: titleBox
anchors {
top: parent.top
left: parent.left
right: parent.right
}
height: Style.main.fontSize *2
color : Style.transparent
Text {
id: textTitleFrom
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: {
if (listview.currentItem === null) return 0
else return listview.currentItem.leftMargin1
}
}
text: "<b>"+qsTr("From:")+"</b> " + root.titleFrom
color: Style.main.text
width: listview.currentItem === null ? 0 : (listview.currentItem.leftMargin2 - listview.currentItem.leftMargin1 - Style.dialog.spacing)
elide: Text.ElideMiddle
}
Text {
id: textTitleTo
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: {
if (listview.currentIndex<0) return root.width/3
else return listview.currentItem.leftMargin2
}
}
text: "<b>"+qsTr("To:")+"</b> " + root.titleTo
color: Style.main.text
}
}
Rectangle {
id: line
anchors {
left : titleBox.left
right : titleBox.right
top : titleBox.bottom
}
height: Style.dialog.borderInput
color: Style.main.line
}
}

View File

@ -0,0 +1,129 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Row {
id: dateRange
property var structure : transferRules
property string sourceID : "-1"
property alias allDates : allDatesBox.checked
property alias inputDateFrom : inputDateFrom
property alias inputDateTo : inputDateTo
property alias labelWidth: label.width
function getRange() {common.getRange()}
function applyRange() {common.applyRange()}
DateRangeFunctions {id:common}
spacing: Style.dialog.spacing*2
Text {
id: label
anchors.verticalCenter: parent.verticalCenter
text : qsTr("Date range")
font {
bold: true
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
color: Style.main.text
}
DateInput {
id: inputDateFrom
label: ""
anchors.verticalCenter: parent.verticalCenter
currentDate: new Date(0) // default epoch start
maxDate: inputDateTo.currentDate
}
Text {
text : Style.fa.arrows_h
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Style.main.text
font.family: Style.fontawesome.name
}
DateInput {
id: inputDateTo
label: ""
anchors.verticalCenter: parent.verticalCenter
currentDate: new Date() // default now
minDate: inputDateFrom.currentDate
isMaxDateToday: true
}
CheckBoxLabel {
id: allDatesBox
text : qsTr("All dates")
anchors.verticalCenter : parent.verticalCenter
checkedSymbol : Style.fa.toggle_on
uncheckedSymbol : Style.fa.toggle_off
uncheckedColor : Style.main.textDisabled
symbolPointSize : Style.dialog.iconSize * Style.pt * 1.1
spacing : Style.dialog.spacing*2
TextMetrics {
id: metrics
text: allDatesBox.checkedSymbol
font {
family: Style.fontawesome.name
pointSize: allDatesBox.symbolPointSize
}
}
Rectangle {
color: allDatesBox.checked ? dotBackground.color : Style.exporting.sliderBackground
width: metrics.width*0.9
height: metrics.height*0.6
radius: height/2
z: -1
anchors {
left: allDatesBox.left
verticalCenter: allDatesBox.verticalCenter
leftMargin: 0.05 * metrics.width
}
Rectangle {
id: dotBackground
color : Style.exporting.background
height : parent.height
width : height
radius : height/2
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
}
}
}
}
}

View File

@ -0,0 +1,227 @@
// 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/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Row {
id: root
spacing: Style.dialog.spacing
property alias labelWidth : label.width
property string labelName : ""
property string labelColor : ""
property alias labelSelected : masterLabelCheckbox.checked
Text {
id: label
text : qsTr("Add import label")
font {
bold: true
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
color: Style.main.text
anchors.verticalCenter: parent.verticalCenter
}
InfoToolTip {
info: qsTr( "When master import lablel is selected then all imported email will have this label.", "Tooltip text for master import label")
anchors.verticalCenter: parent.verticalCenter
}
CheckBoxLabel {
id: masterLabelCheckbox
text : ""
anchors.verticalCenter : parent.verticalCenter
checkedSymbol : Style.fa.toggle_on
uncheckedSymbol : Style.fa.toggle_off
uncheckedColor : Style.main.textDisabled
symbolPointSize : Style.dialog.iconSize * Style.pt * 1.1
spacing : Style.dialog.spacing*2
TextMetrics {
id: metrics
text: masterLabelCheckbox.checkedSymbol
font {
family: Style.fontawesome.name
pointSize: masterLabelCheckbox.symbolPointSize
}
}
Rectangle {
color: parent.checked ? dotBackground.color : Style.exporting.sliderBackground
width: metrics.width*0.9
height: metrics.height*0.6
radius: height/2
z: -1
anchors {
left: masterLabelCheckbox.left
verticalCenter: masterLabelCheckbox.verticalCenter
leftMargin: 0.05 * metrics.width
}
Rectangle {
id: dotBackground
color : Style.exporting.background
height : parent.height
width : height
radius : height/2
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
}
}
}
}
Rectangle {
// label
color : Style.transparent
radius : Style.dialog.radiusButton
border {
color : Style.dialog.line
width : Style.dialog.borderInput
}
anchors.verticalCenter : parent.verticalCenter
scale: area.pressed ? 0.95 : 1
width: content.width
height: content.height
Row {
id: content
spacing : Style.dialog.spacing
padding : Style.dialog.spacing
anchors.verticalCenter: parent.verticalCenter
// label icon color
Text {
text: Style.fa.tag
color: root.labelSelected ? root.labelColor : Style.dialog.line
anchors.verticalCenter: parent.verticalCenter
font {
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
}
TextMetrics {
id:labelMetrics
text: root.labelName
elide: Text.ElideRight
elideWidth:gui.winMain.width*0.303
font {
pointSize: Style.main.fontSize * Style.pt
family: Style.fontawesome.name
}
}
// label text
Text {
text: labelMetrics.elidedText
color: root.labelSelected ? Style.dialog.text : Style.dialog.line
font: labelMetrics.font
anchors.verticalCenter: parent.verticalCenter
}
// edit icon
Text {
text: Style.fa.edit
color: root.labelSelected ? Style.main.textBlue : Style.dialog.line
anchors.verticalCenter: parent.verticalCenter
font {
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
}
}
MouseArea {
id: area
anchors.fill: parent
enabled: root.labelSelected
onClicked : {
if (!root.labelSelected) return
// NOTE: "createLater" is hack
winMain.popupFolderEdit.show(root.labelName, "createLater", root.labelColor, gui.enums.folderTypeLabel, "")
}
}
}
function reset(){
labelColor = go.leastUsedColor()
labelName = qsTr("Imported", "default name of global label followed by date") + " " + gui.niceDateTime()
labelSelected=true
}
Connections {
target: winMain.popupFolderEdit
onEdited : {
if (newName!="") root.labelName = newName
if (newColor!="") root.labelColor = newColor
}
}
/*
SelectLabelsMenu {
id: labelMenu
width : winMain.width/5
sourceID : root.sourceID
selectedIDs : root.structure.getTargetLabelIDs(root.sourceID)
anchors.verticalCenter: parent.verticalCenter
}
LabelIconList {
id: iconList
selectedIDs : root.structure.getTargetLabelIDs(root.sourceID)
anchors.verticalCenter: parent.verticalCenter
}
Connections {
target: structureExternal
onDataChanged: {
iconList.selectedIDs = root.structure.getTargetLabelIDs(root.sourceID)
labelMenu.selectedIDs = root.structure.getTargetLabelIDs(root.sourceID)
}
}
Connections {
target: structurePM
onDataChanged:{
iconList.selectedIDs = root.structure.getTargetLabelIDs(root.sourceID)
labelMenu.selectedIDs = root.structure.getTargetLabelIDs(root.sourceID)
}
}
*/
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List of icons for selected folders
import QtQuick 2.8
import QtQuick.Controls 2.2
import QtQml.Models 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
width: Style.main.fontSize * 2
height: metrics.height
property var colorList
color: "transparent"
DelegateModel {
id: selectedLabels
model : colorList
delegate : Text {
text : metrics.text
font : metrics.font
color : modelData
}
}
TextMetrics {
id: metrics
text: Style.fa.tag
font {
pointSize: Style.main.fontSize * Style.pt
family: Style.fontawesome.name
}
}
Row {
anchors.left : root.left
spacing : {
var n = Math.max(2,root.colorList.length)
var tagWidth = Math.max(1.0,metrics.width)
var space = Math.min(1*Style.px, (root.width - n*tagWidth)/(n-1)) // not more than 1px
space = Math.max(space,-tagWidth) // not less than tag width
return space
}
Repeater {
model: selectedLabels
}
}
}

View File

@ -0,0 +1,475 @@
// 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/>.
// This is main window
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import ImportExportUI 1.0
import ProtonUI 1.0
// Main Window
Window {
id : root
property alias tabbar : tabbar
property alias viewContent : viewContent
property alias viewAccount : viewAccount
property alias dialogAddUser : dialogAddUser
property alias dialogGlobal : dialogGlobal
property alias dialogCredits : dialogCredits
property alias dialogVersionInfo : dialogVersionInfo
property alias dialogUpdate : dialogUpdate
property alias popupMessage : popupMessage
property alias popupFolderEdit : popupFolderEdit
property alias updateState : infoBar.state
property alias dialogExport : dialogExport
property alias dialogImport : dialogImport
property alias addAccountTip : addAccountTip
property int heightContent : height-titleBar.height
property real innerWindowBorder : go.goos=="darwin" ? 0 : Style.main.border
// main window appearance
width : Style.main.width
height : Style.main.height
flags : go.goos=="darwin" ? Qt.Window : Qt.Window | Qt.FramelessWindowHint
color: go.goos=="windows" ? Style.main.background : Style.transparent
title: go.programTitle
minimumWidth : Style.main.width
minimumHeight : Style.main.height
property bool isOutdateVersion : root.updateState == "forceUpgrade"
property bool activeContent :
!dialogAddUser .visible &&
!dialogCredits .visible &&
!dialogVersionInfo .visible &&
!dialogGlobal .visible &&
!dialogUpdate .visible &&
!dialogImport .visible &&
!dialogExport .visible &&
!popupFolderEdit .visible &&
!popupMessage .visible
Accessible.role: Accessible.Grouping
Accessible.description: qsTr("Window %1").arg(title)
Accessible.name: Accessible.description
WindowTitleBar {
id: titleBar
window: root
visible: go.goos!="darwin"
}
Rectangle {
anchors {
top : titleBar.bottom
left : parent.left
right : parent.right
bottom : parent.bottom
}
color: Style.title.background
}
InformationBar {
id: infoBar
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
}
TabLabels {
id: tabbar
currentIndex : 0
enabled: root.activeContent
anchors {
top : infoBar.bottom
right : parent.right
left : parent.left
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
model: [
{ "title" : qsTr("Import-Export" , "title of tab that shows account list" ), "iconText": Style.fa.home },
{ "title" : qsTr("Settings" , "title of tab that allows user to change settings" ), "iconText": Style.fa.cogs },
{ "title" : qsTr("Help" , "title of tab that shows the help menu" ), "iconText": Style.fa.life_ring }
]
}
// Content of tabs
StackLayout {
id: viewContent
enabled: root.activeContent
// dimensions
anchors {
left : parent.left
right : parent.right
top : tabbar.bottom
bottom : parent.bottom
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
bottomMargin: innerWindowBorder
}
// attributes
currentIndex : { return root.tabbar.currentIndex}
clip : true
// content
AccountView {
id : viewAccount
onAddAccount : dialogAddUser.show()
model : accountsModel
hasFooter : false
delegate : AccountDelegate {
row_width : viewContent.width
}
}
SettingsView { id: viewSettings; }
HelpView { id: viewHelp; }
}
// Bubble prevent action
Rectangle {
anchors {
left: parent.left
right: parent.right
top: titleBar.bottom
bottom: parent.bottom
}
visible: bubbleNote.visible
color: "#aa222222"
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
}
BubbleNote {
id : bubbleNote
visible : false
Component.onCompleted : {
bubbleNote.place(0)
}
}
BubbleNote {
id:addAccountTip
anchors.topMargin: viewAccount.separatorNoAccount - 2*Style.main.fontSize
text : qsTr("Click here to start", "on first launch, this is displayed above the Add Account button to tell the user what to do first")
state: (go.isFirstStart && viewAccount.numAccounts==0 && root.viewContent.currentIndex==0) ? "Visible" : "Invisible"
bubbleColor: Style.main.textBlue
Component.onCompleted : {
addAccountTip.place(-1)
}
enabled: false
states: [
State {
name: "Visible"
// hack: opacity 100% makes buttons dialog windows quit wrong color
PropertyChanges{target: addAccountTip; opacity: 0.999; visible: true}
},
State {
name: "Invisible"
PropertyChanges{target: addAccountTip; opacity: 0.0; visible: false}
}
]
transitions: [
Transition {
from: "Visible"
to: "Invisible"
SequentialAnimation{
NumberAnimation {
target: addAccountTip
property: "opacity"
duration: 0
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: addAccountTip
property: "visible"
duration: 0
}
}
},
Transition {
from: "Invisible"
to: "Visible"
SequentialAnimation{
NumberAnimation {
target: addAccountTip
property: "visible"
duration: 300
}
NumberAnimation {
target: addAccountTip
property: "opacity"
duration: 500
easing.type: Easing.InOutQuad
}
}
}
]
}
// Dialogs
DialogAddUser {
id: dialogAddUser
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
onCreateAccount: Qt.openUrlExternally("https://protonmail.com/signup")
}
DialogUpdate {
id: dialogUpdate
title: root.isOutdateVersion ?
qsTr("%1 is outdated", "title of outdate dialog").arg(go.programTitle):
qsTr("%1 update to %2", "title of update dialog").arg(go.programTitle).arg(go.newversion)
introductionText: {
if (root.isOutdateVersion) {
if (go.goos=="linux") {
return qsTr('You are using an outdated version of our software.<br>
Please dowload and install the latest version to continue using %1.<br><br>
<a href="%2">%2</a>',
"Message for force-update in Linux").arg(go.programTitle).arg(go.landingPage)
} else {
return qsTr('You are using an outdated version of our software.<br>
Please dowload and install the latest version to continue using %1.<br><br>
You can continue with update or download and install the new version manually from<br><br>
<a href="%2">%2</a>',
"Message for force-update in Win/Mac").arg(go.programTitle).arg(go.landingPage)
}
} else {
if (go.goos=="linux") {
return qsTr('New version of %1 is available.<br>
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
Use your package manager to update or download and install new version manually from<br><br>
<a href="%4">%4</a>',
"Message for update in Linux").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
} else {
return qsTr('New version of %1 is available.<br>
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
You can continue with update or download and install new version manually from<br><br>
<a href="%4">%4</a>',
"Message for update in Win/Mac").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
}
}
}
}
DialogExport {
id: dialogExport
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
}
DialogImport {
id: dialogImport
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
}
Dialog {
id: dialogCredits
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
title: qsTr("Credits", "title for list of credited libraries")
Credits { }
}
Dialog {
id: dialogVersionInfo
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
property bool checkVersion : false
title: qsTr("Information about", "title of release notes page") + " v" + go.newversion
VersionInfo { }
onShow : {
// Hide information bar with olde version
if ( infoBar.state=="oldVersion" ) {
infoBar.state="upToDate"
dialogVersionInfo.checkVersion = true
}
}
onHide : {
// Reload current version based on online status
if (dialogVersionInfo.checkVersion) go.runCheckVersion(false)
dialogVersionInfo.checkVersion = false
}
}
DialogYesNo {
id: dialogGlobal
question : ""
answer : ""
z: 100
}
PopupEditFolder {
id: popupFolderEdit
anchors {
left: parent.left
right: parent.right
top: infoBar.bottom
bottom: parent.bottom
}
}
// Popup
PopupMessage {
id: popupMessage
anchors {
left : parent.left
right : parent.right
top : infoBar.bottom
bottom : parent.bottom
}
onClickedNo: popupMessage.hide()
onClickedOkay: popupMessage.hide()
onClickedCancel: popupMessage.hide()
onClickedYes: {
if (popupMessage.text == gui.areYouSureYouWantToQuit) Qt.quit()
}
}
// resize
MouseArea { // bottom
id: resizeBottom
property int diff: 0
anchors {
bottom : parent.bottom
left : parent.left
right : parent.right
}
cursorShape: Qt.SizeVerCursor
height: Style.main.fontSize
onPressed: {
var globPos = mapToGlobal(mouse.x, mouse.y)
resizeBottom.diff = root.height
resizeBottom.diff -= globPos.y
}
onMouseYChanged : {
var globPos = mapToGlobal(mouse.x, mouse.y)
root.height = Math.max(root.minimumHeight, globPos.y + resizeBottom.diff)
}
}
MouseArea { // right
id: resizeRight
property int diff: 0
anchors {
top : titleBar.bottom
bottom : parent.bottom
right : parent.right
}
cursorShape: Qt.SizeHorCursor
width: Style.main.fontSize/2
onPressed: {
var globPos = mapToGlobal(mouse.x, mouse.y)
resizeRight.diff = root.width
resizeRight.diff -= globPos.x
}
onMouseXChanged : {
var globPos = mapToGlobal(mouse.x, mouse.y)
root.width = Math.max(root.minimumWidth, globPos.x + resizeRight.diff)
}
}
function showAndRise(){
go.loadAccounts()
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
// Toggle window
function toggle() {
go.loadAccounts()
if (root.visible) {
if (!root.active) {
root.raise()
root.requestActivate()
} else {
root.hide()
}
} else {
root.show()
root.raise()
}
}
onClosing : {
close.accepted=false
if (
(dialogImport.visible && dialogImport.currentIndex == 4 && go.progress!=1) ||
(dialogExport.visible && dialogExport.currentIndex == 2 && go.progress!=1)
) {
popupMessage.buttonOkay .visible = false
popupMessage.buttonYes .visible = false
popupMessage.buttonQuit .visible = true
popupMessage.buttonCancel .visible = true
popupMessage.show ( gui.areYouSureYouWantToQuit )
return
}
close.accepted=true
go.processFinished()
}
}

View File

@ -0,0 +1,84 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Column {
spacing: Style.dialog.spacing
property string checkedText : group.checkedButton.text
Text {
id: formatLabel
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
color: Style.dialog.text
text: qsTr("Select format of exported email:")
InfoToolTip {
info: qsTr("MBOX exports one file for each folder", "todo") + "\n" + qsTr("EML exports one file for each email", "todo")
anchors {
left: parent.right
leftMargin: Style.dialog.spacing
verticalCenter: parent.verticalCenter
}
}
}
Row {
spacing : Style.main.leftMargin
ButtonGroup {
id: group
}
Repeater {
model: [ "MBOX", "EML" ]
delegate : RadioButton {
id: radioDelegate
checked: modelData=="MBOX"
width: 5*Style.dialog.fontSize // hack due to bold
text: modelData
ButtonGroup.group: group
spacing: Style.main.spacing
indicator: Text {
text : radioDelegate.checked ? Style.fa.check_circle : Style.fa.circle_o
color : radioDelegate.checked ? Style.main.textBlue : Style.main.textInactive
font {
pointSize: Style.dialog.iconSize * Style.pt
family: Style.fontawesome.name
}
anchors.verticalCenter: parent.verticalCenter
}
contentItem: Text {
text: radioDelegate.text
color: Style.main.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: checked
}
horizontalAlignment : Text.AlignHCenter
verticalAlignment : Text.AlignVCenter
leftPadding: Style.dialog.iconSize
}
}
}
}
}

View File

@ -0,0 +1,311 @@
// 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/>.
// popup to edit folders or labels
import QtQuick 2.8
import QtQuick.Controls 2.1
import ImportExportUI 1.0
import ProtonUI 1.0
Rectangle {
id: root
visible: false
color: "#aa223344"
property string folderType : gui.enums.folderTypeFolder
property bool isFolder : folderType == gui.enums.folderTypeFolder
property bool isNew : currentId == ""
property bool isCreateLater : currentId == "createLater" // NOTE: "createLater" is hack because folder id should be base64 string
property string currentName : ""
property string currentId : ""
property string currentColor : ""
property string sourceID : ""
property string selectedColor : colorList[0]
property color textColor : Style.main.background
property color backColor : Style.bubble.paneBackground
signal edited(string newName, string newColor)
property var colorList : [ "#7272a7", "#8989ac", "#cf5858", "#cf7e7e", "#c26cc7", "#c793ca", "#7569d1", "#9b94d1", "#69a9d1", "#a8c4d5", "#5ec7b7", "#97c9c1", "#72bb75", "#9db99f", "#c3d261", "#c6cd97", "#e6c04c", "#e7d292", "#e6984c", "#dfb286" ]
MouseArea { // prevent action below aka modal: true
anchors.fill: parent
hoverEnabled: true
}
Rectangle {
id:background
anchors {
fill: root
leftMargin: winMain.width/6
topMargin: winMain.height/6
rightMargin: anchors.leftMargin
bottomMargin: anchors.topMargin
}
color: backColor
radius: Style.errorDialog.radius
}
Column { // content
anchors {
top : background.top
horizontalCenter : background.horizontalCenter
}
topPadding : Style.main.topMargin
bottomPadding : topPadding
spacing : (background.height - title.height - inputField.height - view.height - buttonRow.height - topPadding - bottomPadding) / children.length
Text {
id: title
font.pointSize: Style.dialog.titleSize * Style.pt
color: textColor
text: {
if ( root.isFolder && root.isNew ) return qsTr ( "Create new folder" )
if ( !root.isFolder && root.isNew ) return qsTr ( "Create new label" )
if ( root.isFolder && !root.isNew ) return qsTr ( "Edit folder %1" ) .arg( root.currentName )
if ( !root.isFolder && !root.isNew ) return qsTr ( "Edit label %1" ) .arg( root.currentName )
}
width : parent.width
elide : Text.ElideRight
horizontalAlignment : Text.AlignHCenter
Rectangle {
anchors {
top: parent.bottom
topMargin: Style.dialog.spacing
horizontalCenter: parent.horizontalCenter
}
color: textColor
height: Style.main.borderInput
}
}
TextField {
id: inputField
anchors {
horizontalCenter: parent.horizontalCenter
}
width : parent.width
height : Style.dialog.button
rightPadding : Style.dialog.spacing
leftPadding : height + rightPadding
bottomPadding : rightPadding
topPadding : rightPadding
selectByMouse : true
color : textColor
font.pointSize : Style.dialog.fontSize * Style.pt
background: Rectangle {
color: backColor
border {
color: textColor
width: Style.dialog.borderInput
}
radius : Style.dialog.radiusButton
Text {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
font {
family: Style.fontawesome.name
pointSize: Style.dialog.titleSize * Style.pt
}
text : folderType == gui.enums.folderTypeFolder ? Style.fa.folder : Style.fa.tag
color : root.selectedColor
width : parent.height
horizontalAlignment: Text.AlignHCenter
}
Rectangle {
anchors {
left: parent.left
top: parent.top
leftMargin: parent.height
}
width: parent.border.width/2
height: parent.height
}
}
}
GridView {
id: view
anchors {
horizontalCenter: parent.horizontalCenter
}
model : colorList
cellWidth : 2*Style.dialog.titleSize
cellHeight : cellWidth
width : 10*cellWidth
height : 2*cellHeight
delegate: Rectangle {
width: view.cellWidth*0.8
height: width
radius: width/2
color: modelData
border {
color: indicator.visible ? textColor : modelData
width: 2*Style.px
}
Text {
id: indicator
anchors.centerIn : parent
text: Style.fa.check
color: textColor
font {
family: Style.fontawesome.name
pointSize: Style.dialog.titleSize * Style.pt
}
visible: modelData == root.selectedColor
}
MouseArea {
anchors.fill: parent
onClicked : {
root.selectedColor = modelData
}
}
}
}
Row {
id: buttonRow
anchors {
horizontalCenter: parent.horizontalCenter
}
spacing: Style.main.leftMargin
ButtonRounded {
text: "Cancel"
color_main : textColor
onClicked :{
root.hide()
}
}
ButtonRounded {
text: "Okay"
color_main: Style.dialog.background
color_minor: Style.dialog.textBlue
isOpaque: true
onClicked :{
root.okay()
}
}
}
}
function hide() {
root.visible=false
root.currentId = ""
root.currentName = ""
root.currentColor = ""
root.folderType = ""
root.sourceID = ""
inputField.text = ""
}
function show(currentName, currentId, currentColor, folderType, sourceID) {
root.currentId = currentId
root.currentName = currentName
root.currentColor = currentColor=="" ? go.leastUsedColor() : currentColor
root.selectedColor = root.currentColor
root.folderType = folderType
root.sourceID = sourceID
inputField.text = currentName
root.visible=true
//console.log(title.text , root.currentName, root.currentId, root.currentColor, root.folderType, root.sourceID)
}
function okay() {
// check inpupts
if (inputField.text == "") {
go.notifyError(gui.enums.errFillFolderName)
return
}
if (colorList.indexOf(root.selectedColor)<0) {
go.notifyError(gui.enums.errSelectFolderColor)
return
}
var isLabel = root.folderType == gui.enums.folderTypeLabel
if (!isLabel && !root.isFolder){
console.log("Unknown folder type: ", root.folderType)
go.notifyError(gui.enums.errUpdateLabelFailed)
root.hide()
return
}
if (winMain.dialogImport.address == "") {
console.log("Unknown address", winMain.dialogImport.address)
go.onNotifyError(gui.enums.errUpdateLabelFailed)
root.hide()
}
if (root.isCreateLater) {
root.edited(inputField.text, root.selectedColor)
root.hide()
return
}
// TODO send request (as timer)
if (root.isNew) {
var isOK = go.createLabelOrFolder(winMain.dialogImport.address, inputField.text, root.selectedColor, isLabel, root.sourceID)
if (isOK) {
root.hide()
}
} else {
// TODO: check there was some change
go.updateLabelOrFolder(winMain.dialogImport.address, root.currentId, inputField.text, root.selectedColor)
}
// waiting for finish
// TODO: waiting wheel of doom
// TODO: on close add source to sourceID
}
}

View File

@ -0,0 +1,339 @@
// 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/>.
// This is global combo box which can be adjusted to choose folder target, folder label or global label
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
ComboBox {
id: root
//fixme rounded
height: Style.main.fontSize*2 //fixme
property string folderType: gui.enums.folderTypeFolder
property string sourceID
property var targets
property bool isFolderType: root.folderType == gui.enums.folderTypeFolder
property bool below: true
signal doNotImport()
signal importToFolder(string newTargetID)
signal addTargetLabel(string newTargetID)
signal removeTargetLabel(string newTargetID)
leftPadding: Style.dialog.spacing
onDownChanged : {
root.below = popup.y>0
}
contentItem : Text {
id: boxText
verticalAlignment: Text.AlignVCenter
font {
family: Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
bold: root.down
}
elide: Text.ElideRight
textFormat: Text.StyledText
text : root.displayText
color: !root.enabled ? Style.main.textDisabled : ( root.down ? Style.main.background : Style.main.text )
}
displayText: {
if (view.currentIndex >= 0) {
if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels")
var tgtName = view.currentItem.folderName
var tgtIcon = view.currentItem.folderIcon
var tgtColor = view.currentItem.folderColor
if (tgtIcon != Style.fa.folder_open) {
return tgtIcon + " " + tgtName
}
return '<font color="'+tgtColor+'">'+ tgtIcon + "</font> " + tgtName
}
if (root.isFolderType) return qsTr("No folder selected")
return qsTr("No labels selected")
}
background : RoundedRectangle {
fillColor : root.down ? Style.main.textBlue : Style.transparent
strokeColor : root.down ? fillColor : Style.main.line
radiusTopLeft : root.down && !root.below ? 0 : Style.dialog.radiusButton
radiusBottomLeft : root.down && root.below ? 0 : Style.dialog.radiusButton
radiusTopRight : radiusTopLeft
radiusBottomRight : radiusBottomLeft
MouseArea {
anchors.fill: parent
onClicked : {
if (root.down) root.popup.close()
else root.popup.open()
}
}
}
indicator : Text {
text: (root.down && root.below) || (!root.down && !root.below) ? Style.fa.chevron_up : Style.fa.chevron_down
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: Style.dialog.spacing
}
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
color: root.enabled && !root.down ? Style.main.textBlue : root.contentItem.color
}
// Popup row
delegate: Rectangle {
id: thisDelegate
height : Style.main.fontSize * 2
width : selectNone.width
property bool isHovered: area.containsMouse
color: isHovered ? root.popup.hoverColor : root.popup.backColor
property bool isSelected : isActive
property string folderName: name
property string folderIcon: gui.folderIcon(name,type)
property string folderColor: (type == gui.enums.folderTypeLabel || type == gui.enums.folderTypeFolder) ? iconColor : root.popup.textColor
Text {
id: targetIcon
text: thisDelegate.folderIcon
color : thisDelegate.folderColor
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: root.leftPadding
}
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
Text {
id: targetName
anchors {
verticalCenter: parent.verticalCenter
left: targetIcon.right
right: parent.right
leftMargin: Style.dialog.spacing
rightMargin: Style.dialog.spacing
}
text: thisDelegate.folderName
color : root.popup.textColor
elide: Text.ElideRight
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
Text {
id: targetIndicator
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
text : thisDelegate.isSelected ? Style.fa.check_square : Style.fa.square_o
visible : thisDelegate.isSelected || !root.isFolderType
color : root.popup.textColor
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
Rectangle {
id: line
anchors {
bottom : parent.bottom
left : parent.left
right : parent.right
}
height : Style.main.lineWidth
color : Style.main.line
}
MouseArea {
id: area
anchors.fill: parent
onClicked: {
//console.log(" click delegate")
if (root.isFolderType) { // don't update if selected
root.popup.close()
if (!isActive) {
root.importToFolder(mboxID)
}
} else {
if (isActive) {
root.removeTargetLabel(mboxID)
} else {
root.addTargetLabel(mboxID)
}
}
}
hoverEnabled: true
}
}
popup : Popup {
y: root.height
width: root.width
modal: true
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
padding: Style.dialog.spacing
property var textColor : Style.main.background
property var backColor : Style.main.text
property var hoverColor : Style.main.textBlue
contentItem : Column {
// header
Rectangle {
id: selectNone
width: root.popup.width - 2*root.popup.padding
//height: root.isFolderType ? 2* Style.main.fontSize : 0
height: 2*Style.main.fontSize
color: area.containsMouse ? root.popup.hoverColor : root.popup.backColor
visible : root.isFolderType
Text {
anchors {
left : parent.left
leftMargin : Style.dialog.spacing
verticalCenter : parent.verticalCenter
}
text: root.isFolderType ? qsTr("Do not import") : ""
color: root.popup.textColor
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
}
Rectangle {
id: line
anchors {
bottom : parent.bottom
left : parent.left
right : parent.right
}
height : Style.dialog.borderInput
color : Style.main.line
}
MouseArea {
id: area
anchors.fill: parent
onClicked: {
//console.log(" click no set")
root.doNotImport()
root.popup.close()
}
hoverEnabled: true
}
}
// scroll area
Rectangle {
width: selectNone.width
height: winMain.height/4
color: root.popup.backColor
ListView {
id: view
clip : true
anchors.fill : parent
model : root.targets
delegate : root.delegate
currentIndex: view.model.selectedIndex
}
}
// footer
Rectangle {
id: addFolderOrLabel
width: selectNone.width
height: addButton.height + 3*Style.dialog.spacing
color: root.popup.backColor
Rectangle {
anchors {
top : parent.top
left : parent.left
right : parent.right
}
height : Style.dialog.borderInput
color : Style.main.line
}
ButtonRounded {
id: addButton
anchors.centerIn: addFolderOrLabel
width: parent.width * 0.681
fa_icon : Style.fa.plus_circle
text : root.isFolderType ? qsTr("Create new folder") : qsTr("Create new label")
color_main : root.popup.textColor
}
MouseArea {
anchors.fill : parent
onClicked : {
//console.log("click", addButton.text)
var newName = name
winMain.popupFolderEdit.show(newName, "", "", root.folderType, sourceID)
root.popup.close()
}
}
}
}
background : RoundedRectangle {
strokeColor : root.popup.backColor
fillColor : root.popup.backColor
radiusTopLeft : root.below ? 0 : Style.dialog.radiusButton
radiusBottomLeft : !root.below ? 0 : Style.dialog.radiusButton
radiusTopRight : radiusTopLeft
radiusBottomRight : radiusBottomLeft
}
}
}

View File

@ -0,0 +1,29 @@
// 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/>.
// List of import folder and their target
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
SelectFolderMenu {
id: root
folderType: gui.enums.folderTypeLabel
}

View File

@ -0,0 +1,148 @@
// 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/>.
// List the settings
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
// must have wrapper
Rectangle {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
color: Style.main.background
// content
Column {
anchors.left : parent.left
ButtonIconText {
id: cacheKeychain
text: qsTr("Clear Keychain")
leftIcon.text : Style.fa.chain_broken
rightIcon {
text : qsTr("Clear")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogGlobal.state="clearChain"
dialogGlobal.show()
}
}
ButtonIconText {
id: logs
anchors.left: parent.left
text: qsTr("Logs")
leftIcon.text : Style.fa.align_justify
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: go.openLogs()
}
ButtonIconText {
id: bugreport
anchors.left: parent.left
text: qsTr("Report Bug")
leftIcon.text : Style.fa.bug
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: bugreportWin.show()
}
/*
ButtonIconText {
id: cacheClear
text: qsTr("Clear Cache")
leftIcon.text : Style.fa.times
rightIcon {
text : qsTr("Clear")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogGlobal.state="clearCache"
dialogGlobal.show()
}
}
ButtonIconText {
id: autoStart
text: qsTr("Automatically Start Bridge")
leftIcon.text : Style.fa.rocket
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : go.isAutoStart!=0 ? Style.fa.toggle_on : Style.fa.toggle_off
color : go.isAutoStart!=0 ? Style.main.textBlue : Style.main.textDisabled
}
onClicked: {
go.toggleAutoStart()
}
}
ButtonIconText {
id: advancedSettings
property bool isAdvanced : !go.isDefaultPort
text: qsTr("Advanced settings")
leftIcon.text : Style.fa.cogs
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : isAdvanced!=0 ? Style.fa.chevron_circle_up : Style.fa.chevron_circle_right
color : isAdvanced!=0 ? Style.main.textDisabled : Style.main.textBlue
}
onClicked: {
isAdvanced = !isAdvanced
}
}
ButtonIconText {
id: changePort
visible: advancedSettings.isAdvanced
text: qsTr("Change SMTP/IMAP Ports")
leftIcon.text : Style.fa.plug
rightIcon {
text : qsTr("Change")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogChangePort.show()
}
}
*/
}
}
}

View File

@ -0,0 +1,125 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// credits
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
Rectangle {
id: wrapper
anchors.centerIn: parent
width: 2*Style.main.width/3
height: Style.main.height - 6*Style.dialog.titleSize
color: "transparent"
Flickable {
anchors.fill : wrapper
contentWidth : wrapper.width
contentHeight : content.height
flickableDirection : Flickable.VerticalFlick
clip : true
Column {
id: content
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: wrapper.width
spacing: 5
Text {
visible: go.changelog != ""
anchors {
left: parent.left
}
font.bold: true
font.pointSize: Style.main.fontSize * Style.pt
color: Style.main.text
text: qsTr("Release notes:")
}
Text {
anchors {
left: parent.left
leftMargin: Style.main.leftMargin
}
font.pointSize: Style.main.fontSize * Style.pt
width: wrapper.width - anchors.leftMargin
wrapMode: Text.Wrap
color: Style.main.text
text: go.changelog
}
Text {
visible: go.bugfixes != ""
anchors {
left: parent.left
}
font.bold: true
font.pointSize: Style.main.fontSize * Style.pt
color: Style.main.text
text: qsTr("Fixed bugs:")
}
Repeater {
anchors.fill: parent
model: go.bugfixes.split(";")
Text {
visible: go.bugfixes!=""
anchors {
left: parent.left
leftMargin: Style.main.leftMargin
}
font.pointSize: Style.main.fontSize * Style.pt
width: wrapper.width - anchors.leftMargin
wrapMode: Text.Wrap
color: Style.main.text
text: modelData
}
}
Rectangle{id:spacer; color:"transparent"; width:10; height: buttonClose.height}
ButtonRounded {
id: buttonClose
anchors.horizontalCenter: content.horizontalCenter
text: "Close"
onClicked: {
root.parent.hide()
}
}
AccessibleSelectableText {
anchors.horizontalCenter: content.horizontalCenter
font {
pointSize : Style.main.fontSize * Style.pt
}
color: Style.main.textDisabled
text: "\n Current: "+go.fullversion
}
}
}
}
}

View File

@ -0,0 +1,31 @@
module ImportExportUI
AccountDelegate 1.0 AccountDelegate.qml
Credits 1.0 Credits.qml
DateBox 1.0 DateBox.qml
DateInput 1.0 DateInput.qml
DateRangeMenu 1.0 DateRangeMenu.qml
DateRange 1.0 DateRange.qml
DateRangeFunctions 1.0 DateRangeFunctions.qml
DialogExport 1.0 DialogExport.qml
DialogImport 1.0 DialogImport.qml
DialogYesNo 1.0 DialogYesNo.qml
ExportStructure 1.0 ExportStructure.qml
FilterStructure 1.0 FilterStructure.qml
FolderRowButton 1.0 FolderRowButton.qml
HelpView 1.0 HelpView.qml
IEStyle 1.0 IEStyle.qml
ImportDelegate 1.0 ImportDelegate.qml
ImportSourceButton 1.0 ImportSourceButton.qml
ImportStructure 1.0 ImportStructure.qml
ImportReport 1.0 ImportReport.qml
ImportReportCell 1.0 ImportReportCell.qml
InlineDateRange 1.0 InlineDateRange.qml
InlineLabelSelect 1.0 InlineLabelSelect.qml
LabelIconList 1.0 LabelIconList.qml
MainWindow 1.0 MainWindow.qml
OutputFormat 1.0 OutputFormat.qml
PopupEditFolder 1.0 PopupEditFolder.qml
SelectFolderMenu 1.0 SelectFolderMenu.qml
SelectLabelsMenu 1.0 SelectLabelsMenu.qml
SettingsView 1.0 SettingsView.qml
VersionInfo 1.0 VersionInfo.qml

View File

@ -87,7 +87,7 @@ Item {
Text { // Status
anchors {
left : parent.left
leftMargin : Style.accounts.leftMargin2
leftMargin : viewContent.width/2
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0
@ -99,7 +99,7 @@ Item {
Text { // Actions
anchors {
left : parent.left
leftMargin : Style.accounts.leftMargin3
leftMargin : 5.5*viewContent.width/8
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0

View File

@ -327,6 +327,7 @@ Window {
function show() {
prefill()
description.focus=true
root.visible=true
}

View File

@ -30,10 +30,12 @@ CheckBox {
property color uncheckedColor : Style.main.textInactive
property string checkedSymbol : Style.fa.check_square_o
property string uncheckedSymbol : Style.fa.square_o
property alias symbolPointSize : symbol.font.pointSize
background: Rectangle {
color: Style.transparent
}
indicator: Text {
id: symbol
text : root.checked ? root.checkedSymbol : root.uncheckedSymbol
color : root.checked ? root.checkedColor : root.uncheckedColor
font {

View File

@ -123,12 +123,12 @@ Dialog {
wrapMode: Text.Wrap
text: {
switch (go.progressDescription) {
case 1: return qsTr("Checking the current version.")
case 2: return qsTr("Downloading the update files.")
case 3: return qsTr("Verifying the update files.")
case 4: return qsTr("Unpacking the update files.")
case 5: return qsTr("Starting the update.")
case 6: return qsTr("Quitting the application.")
case "1": return qsTr("Checking the current version.")
case "2": return qsTr("Downloading the update files.")
case "3": return qsTr("Verifying the update files.")
case "4": return qsTr("Unpacking the update files.")
case "5": return qsTr("Starting the update.")
case "6": return qsTr("Quitting the application.")
default: return ""
}
}
@ -220,7 +220,7 @@ Dialog {
function clear() {
root.hasError = false
go.progress = 0.0
go.progressDescription = 0
go.progressDescription = "0"
}
function finished(hasError) {

View File

@ -60,7 +60,7 @@ Row {
FileDialog {
id: pathDialog
title: root.title + ":"
title: root.title
folder: shortcuts.home
onAccepted: sanitizePath(pathDialog.fileUrl.toString())
selectFolder: true

View File

@ -138,6 +138,11 @@ Column {
}
}
function clear() {
inputField.text = ""
rightIcon = ""
}
function checkNonEmpty() {
if (inputField.text == "") {
rightIcon = Style.fa.exclamation_triangle
@ -154,6 +159,17 @@ Column {
if (root.isPassword) inputField.echoMode = TextInput.Password
}
function checkIsANumber(){
if (/^\d+$/.test(inputField.text)) {
rightIcon = Style.fa.check_circle
return true
}
rightIcon = Style.fa.exclamation_triangle
root.placeholderText = ""
inputField.focus = true
return false
}
function forceFocus() {
inputField.forceActiveFocus()
}

View File

@ -23,9 +23,26 @@ import ProtonUI 1.0
Rectangle {
id: root
color: Style.transparent
property alias text: message.text
property alias text : message.text
property alias checkbox : checkbox
property alias buttonQuit : buttonQuit
property alias buttonOkay : buttonOkay
property alias buttonYes : buttonYes
property alias buttonNo : buttonNo
property alias buttonRetry : buttonRetry
property alias buttonSkip : buttonSkip
property alias buttonCancel : buttonCancel
property alias msgWidth : backgroundInp.width
property string msgID : ""
visible: false
signal clickedOkay()
signal clickedYes()
signal clickedNo()
signal clickedRetry()
signal clickedSkip()
signal clickedCancel()
MouseArea { // prevent action below
anchors.fill: parent
hoverEnabled: true
@ -58,14 +75,29 @@ Rectangle {
wrapMode: Text.Wrap
}
ButtonRounded {
text : qsTr("Okay", "todo")
isOpaque : true
color_main : Style.dialog.background
color_minor : Style.dialog.textBlue
onClicked : root.hide()
CheckBoxLabel {
id: checkbox
text: ""
checked: false
visible: (text != "")
textColor : Style.errorDialog.text
checkedColor: Style.errorDialog.text
uncheckedColor: Style.errorDialog.text
anchors.horizontalCenter : parent.horizontalCenter
}
Row {
spacing: Style.dialog.spacing
anchors.horizontalCenter : parent.horizontalCenter
ButtonRounded { id : buttonQuit ; text : qsTr ( "Stop & quit", "" ) ; onClicked : root.clickedYes ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonNo ; text : qsTr ( "No" , "Button No" ) ; onClicked : root.clickedNo ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonYes ; text : qsTr ( "Yes" , "Button Yes" ) ; onClicked : root.clickedYes ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonRetry ; text : qsTr ( "Retry" , "Button Retry" ) ; onClicked : root.clickedRetry ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonSkip ; text : qsTr ( "Skip" , "Button Skip" ) ; onClicked : root.clickedSkip ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonCancel ; text : qsTr ( "Cancel" , "Button Cancel" ) ; onClicked : root.clickedCancel ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonOkay ; text : qsTr ( "Okay" , "Button Okay" ) ; onClicked : root.clickedOkay ( ) ; visible : true ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
}
}
}
@ -75,7 +107,16 @@ Rectangle {
}
function hide() {
root.state = "Okay"
root.visible=false
root .text = ""
checkbox .text = ""
buttonNo .visible = false
buttonYes .visible = false
buttonRetry .visible = false
buttonSkip .visible = false
buttonCancel .visible = false
buttonOkay .visible = true
}
}

View File

@ -0,0 +1,115 @@
// 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/>.
import QtQuick 2.8
import ProtonUI 1.0
Rectangle {
id: root
color: Style.transparent
property color fillColor : Style.main.background
property color strokeColor : Style.main.line
property real strokeWidth : Style.dialog.borderInput
property real radiusTopLeft : Style.dialog.radiusButton
property real radiusBottomLeft : Style.dialog.radiusButton
property real radiusTopRight : Style.dialog.radiusButton
property real radiusBottomRight : Style.dialog.radiusButton
function paint() {
canvas.requestPaint()
}
onFillColorChanged : root.paint()
onStrokeColorChanged : root.paint()
onStrokeWidthChanged : root.paint()
onRadiusTopLeftChanged : root.paint()
onRadiusBottomLeftChanged : root.paint()
onRadiusTopRightChanged : root.paint()
onRadiusBottomRightChanged : root.paint()
Canvas {
id: canvas
anchors.fill: root
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = root.fillColor
ctx.strokeStyle = root.strokeColor
ctx.lineWidth = root.strokeWidth
var dimensions = {
x: ctx.lineWidth,
y: ctx.lineWidth,
w: canvas.width-2*ctx.lineWidth,
h: canvas.height-2*ctx.lineWidth,
}
var radius = {
tl: root.radiusTopLeft,
tr: root.radiusTopRight,
bl: root.radiusBottomLeft,
br: root.radiusBottomRight,
}
root.roundRect(
ctx,
dimensions,
radius, true, true
)
}
}
// adapted from: https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas/3368118#3368118
function roundRect(ctx, dim, radius, fill, stroke) {
if (typeof stroke == 'undefined') {
stroke = true;
}
if (typeof radius === 'undefined') {
radius = 5;
}
if (typeof radius === 'number') {
radius = {tl: radius, tr: radius, br: radius, bl: radius};
} else {
var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
for (var side in defaultRadius) {
radius[side] = radius[side] || defaultRadius[side];
}
}
ctx.beginPath();
ctx.moveTo(dim.x + radius.tl, dim.y);
ctx.lineTo(dim.x + dim.w - radius.tr, dim.y);
ctx.quadraticCurveTo(dim.x + dim.w, dim.y, dim.x + dim.w, dim.y + radius.tr);
ctx.lineTo(dim.x + dim.w, dim.y + dim.h - radius.br);
ctx.quadraticCurveTo(dim.x + dim.w, dim.y + dim.h, dim.x + dim.w - radius.br, dim.y + dim.h);
ctx.lineTo(dim.x + radius.bl, dim.y + dim.h);
ctx.quadraticCurveTo(dim.x, dim.y + dim.h, dim.x, dim.y + dim.h - radius.bl);
ctx.lineTo(dim.x, dim.y + radius.tl);
ctx.quadraticCurveTo(dim.x, dim.y, dim.x + radius.tl, dim.y);
ctx.closePath();
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
Component.onCompleted: root.paint()
}

View File

@ -221,6 +221,31 @@ QtObject {
property real leftMargin3 : 30 * px
}
property QtObject importing : QtObject {
property color rowBackground : dialog.background
property color rowLine : dialog.line
}
property QtObject dropDownLight: QtObject {
property color background : dialog.background
property color text : dialog.text
property color inactive : dialog.line
property color highlight : dialog.textBlue
property color separator : dialog.line
property color line : dialog.line
property bool labelBold : true
}
property QtObject dropDownDark : QtObject {
property color background : dialog.text
property color text : dialog.background
property color inactive : dialog.line
property color highlight : dialog.textBlue
property color separator : dialog.line
property color line : dialog.line
property bool labelBold : true
}
property int okInfoBar : 0
property int warnInfoBar : 1
property int warnBubbleMessage : 2

View File

@ -23,7 +23,9 @@ import ProtonUI 1.0
Rectangle {
id: root
height: root.isDarwin ? Style.titleMacOS.height : Style.title.height
height: visible ? (
root.isDarwin ? Style.titleMacOS.height : Style.title.height
) : 0
color: "transparent"
property bool isDarwin : (go.goos == "darwin")
property QtObject window

View File

@ -23,6 +23,7 @@ InputField 1.0 InputField.qml
InstanceExistsWindow 1.0 InstanceExistsWindow.qml
LogoHeader 1.0 LogoHeader.qml
PopupMessage 1.0 PopupMessage.qml
RoundedRectangle 1.0 RoundedRectangle.qml
TabButton 1.0 TabButton.qml
TabLabels 1.0 TabLabels.qml
TextLabel 1.0 TextLabel.qml

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
clean:
rm -f moc.cpp
rm -f moc.go
rm -f moc.h
rm -f moc_cgo*.go
rm -f moc_moc.h

View File

@ -0,0 +1,236 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtcommon
import (
"fmt"
"github.com/therecipe/qt/core"
)
// AccountInfo is an element of model. It contains all data for one account and
// it's aliases
type AccountInfo struct {
core.QObject
_ string `property:"account"`
_ string `property:"userID"`
_ string `property:"status"`
_ string `property:"hostname"`
_ string `property:"password"`
_ string `property:"security"`
_ int `property:"portSMTP"`
_ int `property:"portIMAP"`
_ string `property:"aliases"`
_ bool `property:"isExpanded"`
_ bool `property:"isCombinedAddressMode"`
}
// Constants for AccountsModel property map
const (
Account = int(core.Qt__UserRole) + 1<<iota
UserID
Status
Hostname
Password
Security
PortIMAP
PortSMTP
Aliases
IsExpanded
IsCombinedAddressMode
)
// Registration of new metatype before creating instance
// NOTE: check it is run once per program. write a log
func init() {
AccountInfo_QRegisterMetaType()
}
// AccountModel for providing container of accounts information to QML.
// QML ListView connects the model from Go and it shows item (accounts) information.
// Copied and edited from `github.com/therecipe/qt/internal/examples/sailfish/listview`.
type AccountsModel struct {
core.QAbstractListModel
// QtObject Constructor
_ func() `constructor:"init"`
// List of item properties.
// All available item properties are inside the map.
_ map[int]*core.QByteArray `property:"roles"`
// The data storage.
// The slice with all accounts. It is not accessed directly but using `data(index,role)`.
_ []*AccountInfo `property:"accounts"`
// Method for adding account.
_ func(*AccountInfo) `slot:"addAccount"`
// Method for retrieving account.
_ func(row int) *AccountInfo `slot:"get"`
// Method for login/logout the account.
_ func(row int) `slot:"toggleIsAvailable"`
// Method for removing account from list.
_ func(row int) `slot:"removeAccount"`
_ int `property:"count"`
}
// init is called by C constructor. It creates the map for item properties and
// connects the methods.
func (s *AccountsModel) init() {
s.SetRoles(map[int]*core.QByteArray{
Account: NewQByteArrayFromString("account"),
UserID: NewQByteArrayFromString("userID"),
Status: NewQByteArrayFromString("status"),
Hostname: NewQByteArrayFromString("hostname"),
Password: NewQByteArrayFromString("password"),
Security: NewQByteArrayFromString("security"),
PortIMAP: NewQByteArrayFromString("portIMAP"),
PortSMTP: NewQByteArrayFromString("portSMTP"),
Aliases: NewQByteArrayFromString("aliases"),
IsExpanded: NewQByteArrayFromString("isExpanded"),
IsCombinedAddressMode: NewQByteArrayFromString("isCombinedAddressMode"),
})
// Basic QAbstractListModel methods.
s.ConnectData(s.data)
s.ConnectRowCount(s.rowCount)
s.ConnectColumnCount(s.columnCount)
s.ConnectRoleNames(s.roleNames)
// Custom AccountModel methods.
s.ConnectGet(s.get)
s.ConnectAddAccount(s.addAccount)
s.ConnectToggleIsAvailable(s.toggleIsAvailable)
s.ConnectRemoveAccount(s.removeAccount)
}
// get returns account info pointer or create new empy if index is out of
// range.
func (s *AccountsModel) get(index int) *AccountInfo {
if index < 0 || index >= len(s.Accounts()) {
return NewAccountInfo(nil)
}
return s.Accounts()[index]
}
// data return value for index and property
func (s *AccountsModel) data(index *core.QModelIndex, property int) *core.QVariant {
if !index.IsValid() {
return core.NewQVariant()
}
if index.Row() >= len(s.Accounts()) {
return core.NewQVariant()
}
var accountInfo = s.Accounts()[index.Row()]
switch property {
case Account:
return NewQVariantString(accountInfo.Account())
case UserID:
return NewQVariantString(accountInfo.UserID())
case Status:
return NewQVariantString(accountInfo.Status())
case Hostname:
return NewQVariantString(accountInfo.Hostname())
case Password:
return NewQVariantString(accountInfo.Password())
case Security:
return NewQVariantString(accountInfo.Security())
case PortIMAP:
return NewQVariantInt(accountInfo.PortIMAP())
case PortSMTP:
return NewQVariantInt(accountInfo.PortSMTP())
case Aliases:
return NewQVariantString(accountInfo.Aliases())
case IsExpanded:
return NewQVariantBool(accountInfo.IsExpanded())
case IsCombinedAddressMode:
return NewQVariantBool(accountInfo.IsCombinedAddressMode())
default:
return core.NewQVariant()
}
}
// rowCount returns the dimension of model: number of rows is equivalent to number of items in list.
func (s *AccountsModel) rowCount(parent *core.QModelIndex) int {
return len(s.Accounts())
}
// columnCount returns the dimension of model: AccountsModel has only one column.
func (s *AccountsModel) columnCount(parent *core.QModelIndex) int {
return 1
}
// roleNames returns the names of available item properties.
func (s *AccountsModel) roleNames() map[int]*core.QByteArray {
return s.Roles()
}
// addAccount is connected to the addAccount slot.
func (s *AccountsModel) addAccount(accountInfo *AccountInfo) {
s.BeginInsertRows(core.NewQModelIndex(), len(s.Accounts()), len(s.Accounts()))
s.SetAccounts(append(s.Accounts(), accountInfo))
s.SetCount(len(s.Accounts()))
s.EndInsertRows()
}
// toggleIsAvailable is connected to toggleIsAvailable slot.
func (s *AccountsModel) toggleIsAvailable(row int) {
var accountInfo = s.Accounts()[row]
currentStatus := accountInfo.Status()
if currentStatus == "active" {
accountInfo.SetStatus("disabled")
} else if currentStatus == "disabled" {
accountInfo.SetStatus("active")
} else {
accountInfo.SetStatus("error")
}
var pIndex = s.Index(row, 0, core.NewQModelIndex())
s.DataChanged(pIndex, pIndex, []int{Status})
}
// removeAccount is connected to removeAccount slot.
func (s *AccountsModel) removeAccount(row int) {
s.BeginRemoveRows(core.NewQModelIndex(), row, row)
s.SetAccounts(append(s.Accounts()[:row], s.Accounts()[row+1:]...))
s.SetCount(len(s.Accounts()))
s.EndRemoveRows()
}
// Clear removes all items in model.
func (s *AccountsModel) Clear() {
s.BeginRemoveRows(core.NewQModelIndex(), 0, len(s.Accounts()))
s.SetAccounts(s.Accounts()[0:0])
s.SetCount(len(s.Accounts()))
s.EndRemoveRows()
}
// Dump prints the content of account models to console.
func (s *AccountsModel) Dump() {
fmt.Printf("Dimensions rows %d cols %d\n", s.rowCount(nil), s.columnCount(nil))
for iAcc := 0; iAcc < s.rowCount(nil); iAcc++ {
var accountInfo = s.Accounts()[iAcc]
fmt.Printf(" %d. %s\n", iAcc, accountInfo.Account())
}
}

View File

@ -0,0 +1,259 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtcommon
import (
"fmt"
"strings"
"sync"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
// QMLer sends signals to GUI
type QMLer interface {
ProcessFinished()
NotifyHasNoKeychain()
SetConnectionStatus(bool)
SetIsRestarting(bool)
SetAddAccountWarning(string, int)
NotifyBubble(int, string)
EmitEvent(string, string)
Quit()
CanNotReachAPI() string
WrongMailboxPassword() string
}
// Accounts holds functionality of users
type Accounts struct {
Model *AccountsModel
qml QMLer
um types.UserManager
prefs *config.Preferences
authClient pmapi.Client
auth *pmapi.Auth
LatestUserID string
accountMutex sync.Mutex
}
// SetupAccounts will create Model and set QMLer and UserManager
func (a *Accounts) SetupAccounts(qml QMLer, um types.UserManager) {
a.Model = NewAccountsModel(nil)
a.qml = qml
a.um = um
}
// LoadAccounts refreshes the current account list in GUI
func (a *Accounts) LoadAccounts() {
a.accountMutex.Lock()
defer a.accountMutex.Unlock()
a.Model.Clear()
users := a.um.GetUsers()
// If there are no active accounts.
if len(users) == 0 {
log.Info("No active accounts")
return
}
for _, user := range users {
accInfo := NewAccountInfo(nil)
username := user.Username()
if username == "" {
username = user.ID()
}
accInfo.SetAccount(username)
// Set status.
if user.IsConnected() {
accInfo.SetStatus("connected")
} else {
accInfo.SetStatus("disconnected")
}
// Set login info.
accInfo.SetUserID(user.ID())
accInfo.SetHostname(bridge.Host)
accInfo.SetPassword(user.GetBridgePassword())
if a.prefs != nil {
accInfo.SetPortIMAP(a.prefs.GetInt(preferences.IMAPPortKey))
accInfo.SetPortSMTP(a.prefs.GetInt(preferences.SMTPPortKey))
}
// Set aliases.
accInfo.SetAliases(strings.Join(user.GetAddresses(), ";"))
accInfo.SetIsExpanded(user.ID() == a.LatestUserID)
accInfo.SetIsCombinedAddressMode(user.IsCombinedAddressMode())
a.Model.addAccount(accInfo)
}
// Updated can clear.
a.LatestUserID = ""
}
// ClearCache signal to remove all DB files
func (a *Accounts) ClearCache() {
defer a.qml.ProcessFinished()
if err := a.um.ClearData(); err != nil {
log.Error("While clearing cache: ", err)
}
// Clearing data removes everything (db, preferences, ...)
// so everything has to be stopped and started again.
a.qml.SetIsRestarting(true)
a.qml.Quit()
}
// ClearKeychain signal remove all accounts from keychains
func (a *Accounts) ClearKeychain() {
defer a.qml.ProcessFinished()
for _, user := range a.um.GetUsers() {
if err := a.um.DeleteUser(user.ID(), false); err != nil {
log.Error("While deleting user: ", err)
if err == keychain.ErrNoKeychainInstalled { // Probably not needed anymore.
a.qml.NotifyHasNoKeychain()
}
}
}
}
// LogoutAccount signal to remove account
func (a *Accounts) LogoutAccount(iAccount int) {
defer a.qml.ProcessFinished()
userID := a.Model.get(iAccount).UserID()
user, err := a.um.GetUser(userID)
if err != nil {
log.Error("While logging out ", userID, ": ", err)
return
}
if err := user.Logout(); err != nil {
log.Error("While logging out ", userID, ": ", err)
}
}
func (a *Accounts) showLoginError(err error, scope string) bool {
if err == nil {
a.qml.SetConnectionStatus(true) // If we are here connection is ok.
return false
}
log.Warnf("%s: %v", scope, err)
if err == pmapi.ErrAPINotReachable {
a.qml.SetConnectionStatus(false)
SendNotification(a.qml, TabAccount, a.qml.CanNotReachAPI())
a.qml.ProcessFinished()
return true
}
a.qml.SetConnectionStatus(true) // If we are here connection is ok.
if err == pmapi.ErrUpgradeApplication {
a.qml.EmitEvent(events.UpgradeApplicationEvent, "")
return true
}
a.qml.SetAddAccountWarning(err.Error(), -1)
return true
}
// Login signal returns:
// -1: when error occurred
// 0: when no 2FA and no MBOX
// 1: when has 2FA
// 2: when has no 2FA but have MBOX
func (a *Accounts) Login(login, password string) int {
var err error
a.authClient, a.auth, err = a.um.Login(login, password)
if a.showLoginError(err, "login") {
return -1
}
if a.auth.HasTwoFactor() {
return 1
}
if a.auth.HasMailboxPassword() {
return 2
}
return 0 // No 2FA, no mailbox password.
}
// Auth2FA returns:
// -1 : error (use SetAddAccountWarning to show message)
// 0 : single password mode
// 1 : two password mode
func (a *Accounts) Auth2FA(twoFacAuth string) int {
var err error
if a.auth == nil || a.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient)
} else {
_, err = a.authClient.Auth2FA(twoFacAuth, a.auth)
}
if a.showLoginError(err, "auth2FA") {
return -1
}
if a.auth.HasMailboxPassword() {
return 1 // Ask for mailbox password.
}
return 0 // One password.
}
// AddAccount signal to add an account. It should close login modal
// ProcessFinished if ok.
func (a *Accounts) AddAccount(mailboxPassword string) int {
if a.auth == nil || a.authClient == nil {
log.Errorf("Missing authentication in addAccount %p %p", a.auth, a.authClient)
a.qml.SetAddAccountWarning(a.qml.WrongMailboxPassword(), -2)
return -1
}
user, err := a.um.FinishLogin(a.authClient, a.auth, mailboxPassword)
if err != nil {
log.WithError(err).Error("Login was unsuccessful")
a.qml.SetAddAccountWarning("Failure: "+err.Error(), -2)
return -1
}
a.LatestUserID = user.ID()
a.qml.EmitEvent(events.UserRefreshEvent, user.ID())
a.qml.ProcessFinished()
return 0
}
// DeleteAccount by index in Model
func (a *Accounts) DeleteAccount(iAccount int, removePreferences bool) {
defer a.qml.ProcessFinished()
userID := a.Model.get(iAccount).UserID()
if err := a.um.DeleteUser(userID, removePreferences); err != nil {
log.Warn("deleteUser: cannot remove user: ", err)
if err == keychain.ErrNoKeychainInstalled {
a.qml.NotifyHasNoKeychain()
return
}
SendNotification(a.qml, TabSettings, err.Error())
return
}
}

View File

@ -1,23 +1,30 @@
// +build !nogui
#include "logs.h"
#include "common.h"
#include "_cgo_export.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QVector>
#include <QtGlobal>
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
Q_UNUSED(type);
Q_UNUSED(context);
QByteArray localMsg = msg.toUtf8().prepend("WHITESPACE");
Q_UNUSED( type )
Q_UNUSED( context )
QByteArray localMsg = msg.toUtf8().prepend("WHITESPACE");
logMsgPacked(
const_cast<char*>( (localMsg.constData()) +10 ),
localMsg.size()-10
);
//printf("Handler: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
}
void InstallMessageHandler() { qInstallMessageHandler(messageHandler); }
void InstallMessageHandler() {
qInstallMessageHandler(messageHandler);
}
void RegisterTypes() {
qRegisterMetaType<QVector<int> >();
}

View File

@ -0,0 +1,120 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtcommon
//#include "common.h"
import "C"
import (
"bufio"
"os"
"time"
"github.com/sirupsen/logrus"
"github.com/therecipe/qt/core"
)
var log = logrus.WithField("pkg", "frontend/qt-common")
var logQML = logrus.WithField("pkg", "frontend/qml")
// RegisterTypes for vector of ints
func RegisterTypes() { // need to fix test message
C.RegisterTypes()
}
func installMessageHandler() {
C.InstallMessageHandler()
}
//export logMsgPacked
func logMsgPacked(data *C.char, len C.int) {
logQML.Warn(C.GoStringN(data, len))
}
// QtSetupCoreAndControls hanldes global setup of Qt.
// Should be called once per program. Probably once per thread is fine.
func QtSetupCoreAndControls(programName, programVersion string) {
installMessageHandler()
// Core setup.
core.QCoreApplication_SetApplicationName(programName)
core.QCoreApplication_SetApplicationVersion(programVersion)
// High DPI scaling for windows.
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false)
// Software OpenGL: to avoid dedicated GPU.
core.QCoreApplication_SetAttribute(core.Qt__AA_UseSoftwareOpenGL, true)
// Basic style for QuickControls2 objects.
//quickcontrols2.QQuickStyle_SetStyle("material")
}
// NewQByteArrayFromString is wrapper for new QByteArray from string
func NewQByteArrayFromString(name string) *core.QByteArray {
return core.NewQByteArray2(name, -1)
}
// NewQVariantString is wrapper for QVariant alocator String
func NewQVariantString(data string) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantStringArray is wrapper for QVariant alocator String Array
func NewQVariantStringArray(data []string) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantBool is wrapper for QVariant alocator Bool
func NewQVariantBool(data bool) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantInt is wrapper for QVariant alocator Int
func NewQVariantInt(data int) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantLong is wrapper for QVariant alocator Int64
func NewQVariantLong(data int64) *core.QVariant {
return core.NewQVariant1(data)
}
// Pause used to show GUI tests
func Pause() {
time.Sleep(500 * time.Millisecond)
}
// Longer pause used to diplay GUI tests
func PauseLong() {
time.Sleep(3 * time.Second)
}
// FIXME: Not working in test...
func WaitForEnter() {
log.Print("Press 'Enter' to continue...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
}
type Listener interface {
Add(string, chan<- string)
}
func MakeAndRegisterEvent(eventListener Listener, event string) <-chan string {
ch := make(chan string)
eventListener.Add(event, ch)
return ch
}

View File

@ -10,8 +10,9 @@
extern "C" {
#endif // C++
void InstallMessageHandler();
;
void InstallMessageHandler();
void RegisterTypes();
;
#ifdef __cplusplus
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtcommon
// Positions of notification bubble
const (
TabAccount = 0
TabSettings = 1
TabHelp = 2
TabQuit = 4
TabUpdates = 100
TabAddAccount = -1
)
// Notifier show bubble notification at postion marked by int
type Notifier interface {
NotifyBubble(int, string)
}
// SendNotification unifies notification in GUI
func SendNotification(qml Notifier, tabIndex int, msg string) {
qml.NotifyBubble(tabIndex, msg)
}

View File

@ -0,0 +1,60 @@
QMLfiles=$(shell find ../qml/ -name "*.qml") $(shell find ../qml/ -name "qmldir")
FontAwesome=${CURDIR}/../share/fontawesome-webfont.ttf
ImageDir=${CURDIR}/../share/icons
Icons=$(shell find ${ImageDir} -name "*.png")
Icons+= share/images/folder_open.png share/images/envelope_open.png
MocDependencies= ./ui.go ./account_model.go ./folder_structure.go ./folder_functions.go
## EnumDependecies= ../backend/errors/errors.go ../backend/progress.go ../backend/source/enum.go ../frontend/enums.go
all: ../qml/ImportExportUI/images moc.go ../qml/GuiIE.qml qmlcheck rcc.cpp
## ./qml/GuiIE.qml: enums.sh ${EnumDependecies}
## ./enums.sh
../qml/ProtonUI/fontawesome.ttf:
ln -sf ${FontAwesome} $@
../qml/ProtonUI/images:
ln -sf ${ImageDir} $@
../qml/ImportExportUI/images:
ln -sf ${ImageDir} $@
translate.ts: ${QMLfiles}
lupdate -recursive qml/ -ts $@
rcc.cpp: ${QMLfiles} ${Icons} resources.qrc
rm -f rcc.cpp rcc.qrc && qtrcc -o .
qmltest:
qmltestrunner -eventdelay 500 -import ../qml/
qmlcheck: ../qml/ProtonUI/fontawesome.ttf ../qml/ImportExportUI/images ../qml/ProtonUI/images
qmlscene -verbose -I ../qml/ -f ../qml/tst_GuiIE.qml --quit
qmlpreview: ../qml/ProtonUI/fontawesome.ttf ../qml/ImportExportUI/images ../qml/ProtonUI/images
rm -f ../qml/*.qmlc ../qml/ProtonUI/*.qmlc ../qml/ImportExportUI/*.qmlc
qmlscene -verbose -I ../qml/ -f ../qml/tst_GuiIE.qml 2>&1
test: qmlcheck moc.go rcc.cpp
go test -v
moc.go: ${MocDependencies}
qtmoc
clean:
rm -rf linux/
rm -rf darwin/
rm -rf windows/
rm -rf deploy/
rm -f moc.cpp
rm -f moc.go
rm -f moc.h
rm -f moc_cgo*.go
rm -f moc_moc.h
rm -f rcc.cpp
rm -f rcc.qrc
rm -f rcc_cgo*.go
rm -f ../rcc.cpp
rm -f ../rcc.qrc
rm -f ../rcc_cgo*.go
rm -rf ../qml/ProtonUI/images
rm -f ../qml/ProtonUI/fontawesome.ttf
find ../qml -name *.qmlc -exec rm {} \;

View File

@ -0,0 +1,55 @@
# ProtonMail Import-Export Qt interface
Import-Export uses [Qt](https://www.qt.io) framework for creating appealing graphical
user interface. Package [therecipe/qt](https://github.com/therecipe/qt) is used
to implement Qt into [Go](https://www.goglang.com).
# For developers
The GUI is designed inside QML files. Communication with backend is done via
[frontend.go](./frontend.go). The API documentation is done via `go-doc`.
## Setup
* if you don't have the system wide `go-1.8.1` download, install localy (e.g.
`~/build/go-1.8.1`) and setup:
export GOROOT=~/build/go-1.8.1/go
export PATH=$GOROOT/bin:$PATH
* go to your working directory and export `$GOPATH`
export GOPATH=`Pwd`
mkdir -p $GOPATH/bin
export PATH=$PATH:$GOPATH/bin
* if you dont have system wide `Qt-5.8.0`
[download](https://download.qt.io/official_releases/qt/5.8/5.8.0/qt-opensource-linux-x64-5.8.0.run),
install locally (e.g. `~/build/qt/qt-5.8.0`) and setup:
export QT_DIR=~/build/qt/qt-5.8.0
export PATH=$QT_DIR/5.8/gcc_64/bin:$PATH
* `Go-Qt` setup (installation is system dependent see
[therecipe/qt/README](https://github.com/therecipe/qt/blob/master/README.md)
for details)
go get -u -v github.com/therecipe/qt/cmd/...
$GOPATH/bin/qtsetup
## Compile
* it is necessary to compile the Qt-C++ with go for resources and meta-objects
make -f Makefile.local
* FIXME the rcc file is implicitly generated with `package main`. This needs to
be changed to `package qtie` manually
* check that user interface is working
make -f Makefile.local test
## Test
make -f Makefile.local qmlpreview
## Deploy
* before compilation of Import-Export it is necessary to run compilation of Qt-C++ part (done in makefile)

View File

@ -0,0 +1,120 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtie
import (
"github.com/therecipe/qt/core"
)
// Folder Type
const (
FolderTypeSystem = ""
FolderTypeLabel = "label"
FolderTypeFolder = "folder"
FolderTypeExternal = "external"
)
// Status
const (
StatusNoInternet = "noInternet"
StatusCheckingInternet = "internetCheck"
StatusNewVersionAvailable = "oldVersion"
StatusUpToDate = "upToDate"
StatusForceUpdate = "forceupdate"
)
// Constants for data map
const (
// Account info
Account = int(core.Qt__UserRole) + 1 + iota // 256 + 1 = 257
Status // 258
Password // 259
Aliases // ...
IsExpanded
// Folder info
FolderId
FolderName
FolderColor
FolderType
FolderEntries
IsFolderSelected
FolderFromDate
FolderToDate
TargetFolderID
TargetLabelIDs
// Error list
MailSubject
MailDate
MailFrom
InputFolder
ErrorMessage
// Transfer rules mbox
MboxSelectedIndex
MboxIsActive
MboxID
MboxName
MboxType
MboxColor
// Transfer Rules
RuleTargetLabelColors
RuleFromDate
RuleToDate
)
const (
// This should match enums in GuiIE.qml
errUnknownError = 0
errEventAPILogout = 1
errUpdateAPI = 2
errUpdateJSON = 3
errUserAuth = 4
errQApplication = 18
errEmailExportFailed = 6
errEmailExportMissing = 7
errNothingToImport = 8
errEmailImportFailed = 12
errDraftImportFailed = 13
errDraftLabelFailed = 14
errEncryptMessageAttachment = 15
errEncryptMessage = 16
errNoInternetWhileImport = 17
errUnlockUser = 5
errSourceMessageNotSelected = 19
errCannotParseMail = 5000
errWrongLoginOrPassword = 5001
errWrongServerPathOrPort = 5002
errWrongAuthMethod = 5003
errIMAPFetchFailed = 5004
errLocalSourceLoadFailed = 1000
errPMLoadFailed = 1001
errRemoteSourceLoadFailed = 1002
errLoadAccountList = 1005
errExit = 1006
errRetry = 1007
errAsk = 1008
errImportFailed = 1009
errCreateLabelFailed = 1010
errCreateFolderFailed = 1011
errUpdateLabelFailed = 1012
errUpdateFolderFailed = 1013
errFillFolderName = 1014
errSelectFolderColor = 1015
errNoInternet = 1016
)

View File

@ -0,0 +1,100 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtie
import (
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/therecipe/qt/core"
)
func init() {
ErrorListModel_QRegisterMetaType()
}
// ErrorListModel to sending error details to Qt
type ErrorListModel struct {
core.QAbstractListModel
// Qt list model
_ func() `constructor:"init"`
_ map[int]*core.QByteArray `property:"roles"`
_ int `property:"count"`
Progress *transfer.Progress
records []*transfer.MessageStatus
}
func (e *ErrorListModel) init() {
e.SetRoles(map[int]*core.QByteArray{
MailSubject: qtcommon.NewQByteArrayFromString("mailSubject"),
MailDate: qtcommon.NewQByteArrayFromString("mailDate"),
MailFrom: qtcommon.NewQByteArrayFromString("mailFrom"),
InputFolder: qtcommon.NewQByteArrayFromString("inputFolder"),
ErrorMessage: qtcommon.NewQByteArrayFromString("errorMessage"),
})
// basic QAbstractListModel mehods
e.ConnectData(e.data)
e.ConnectRowCount(e.rowCount)
e.ConnectColumnCount(e.columnCount)
e.ConnectRoleNames(e.roleNames)
}
func (e *ErrorListModel) data(index *core.QModelIndex, role int) *core.QVariant {
if !index.IsValid() {
return core.NewQVariant()
}
if index.Row() >= len(e.records) {
return core.NewQVariant()
}
var r = e.records[index.Row()]
switch role {
case MailSubject:
return qtcommon.NewQVariantString(r.Subject)
case MailDate:
return qtcommon.NewQVariantString(r.Time.String())
case MailFrom:
return qtcommon.NewQVariantString(r.From)
case InputFolder:
return qtcommon.NewQVariantString(r.SourceID)
case ErrorMessage:
return qtcommon.NewQVariantString(r.GetErrorMessage())
default:
return core.NewQVariant()
}
}
func (e *ErrorListModel) rowCount(parent *core.QModelIndex) int { return len(e.records) }
func (e *ErrorListModel) columnCount(parent *core.QModelIndex) int { return 1 }
func (e *ErrorListModel) roleNames() map[int]*core.QByteArray { return e.Roles() }
func (e *ErrorListModel) load() {
if e.Progress == nil {
log.Error("Progress not connected")
return
}
e.BeginResetModel()
e.records = e.Progress.GetFailedMessages()
e.EndResetModel()
}

View File

@ -0,0 +1,80 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtie
import (
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/pkg/errors"
)
const (
TypeEML = "EML"
TypeMBOX = "MBOX"
)
func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
errCode := errUnknownError
var err error
defer func() {
if err != nil {
f.showError(errCode, errors.Wrap(err, "failed to load structure for "+addressOrID))
f.Qml.ExportStructureLoadFinished(false)
} else {
f.Qml.ExportStructureLoadFinished(true)
}
}()
if f.transfer, err = f.ie.GetEMLExporter(addressOrID, ""); err != nil {
// The only error can be problem to load PM user and address.
errCode = errPMLoadFailed
return
}
// Export has only one option to set time limits--by global time range.
// In case user changes file or because of some bug global time is saved
// to all rules, let's clear it, because there is no way to show it in
// GUI and user would be confused and see it does not work at all.
for _, rule := range f.transfer.GetRules() {
isActive := rule.Active
f.transfer.SetRule(rule.SourceMailbox, rule.TargetMailboxes, 0, 0)
if !isActive {
f.transfer.UnsetRule(rule.SourceMailbox)
}
}
f.TransferRules.setTransfer(f.transfer)
}
func (f *FrontendQt) StartExport(rootPath, login, fileType string, attachEncryptedBody bool) {
var target transfer.TargetProvider
if fileType == TypeEML {
target = transfer.NewEMLProvider(rootPath)
} else if fileType == TypeMBOX {
target = transfer.NewMBOXProvider(rootPath)
} else {
log.Errorln("Wrong file format:", fileType)
return
}
f.transfer.ChangeTarget(target)
f.transfer.SetSkipEncryptedMessages(!attachEncryptedBody)
progress := f.transfer.Start()
f.setProgressManager(progress)
}

View File

@ -0,0 +1,531 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtie
import (
"errors"
"strings"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/therecipe/qt/core"
)
const (
GlobalOptionIndex = -1
)
var AllFolderInfoRoles = []int{
FolderId,
FolderName,
FolderColor,
FolderType,
FolderEntries,
IsFolderSelected,
FolderFromDate,
FolderToDate,
TargetFolderID,
TargetLabelIDs,
}
func getTargetHashes(mboxes []transfer.Mailbox) (targetFolderID, targetLabelIDs string) {
for _, targetMailbox := range mboxes {
if targetMailbox.IsExclusive {
targetFolderID = targetMailbox.Hash()
} else {
targetLabelIDs += targetMailbox.Hash() + ";"
}
}
targetLabelIDs = strings.Trim(targetLabelIDs, ";")
return
}
func newFolderInfo(mbox transfer.Mailbox, rule *transfer.Rule) *FolderInfo {
targetFolderID, targetLabelIDs := getTargetHashes(rule.TargetMailboxes)
entry := &FolderInfo{
mailbox: mbox,
FolderEntries: 1,
FromDate: rule.FromTime,
ToDate: rule.ToTime,
IsFolderSelected: rule.Active,
TargetFolderID: targetFolderID,
TargetLabelIDs: targetLabelIDs,
}
entry.FolderType = FolderTypeSystem
if !pmapi.IsSystemLabel(mbox.ID) {
if mbox.IsExclusive {
entry.FolderType = FolderTypeFolder
} else {
entry.FolderType = FolderTypeLabel
}
}
return entry
}
func (s *FolderStructure) saveRule(info *FolderInfo) error {
if s.transfer == nil {
return errors.New("missing transfer")
}
sourceMbox := info.mailbox
if !info.IsFolderSelected {
s.transfer.UnsetRule(sourceMbox)
return nil
}
allTargetMboxes, err := s.transfer.TargetMailboxes()
if err != nil {
return err
}
var targetMboxes []transfer.Mailbox
for _, target := range allTargetMboxes {
targetHash := target.Hash()
if info.TargetFolderID == targetHash || strings.Contains(info.TargetLabelIDs, targetHash) {
targetMboxes = append(targetMboxes, target)
}
}
return s.transfer.SetRule(sourceMbox, targetMboxes, info.FromDate, info.ToDate)
}
func (s *FolderInfo) updateTargetLabelIDs(targetLabelsSet map[string]struct{}) {
targets := []string{}
for key := range targetLabelsSet {
targets = append(targets, key)
}
s.TargetLabelIDs = strings.Join(targets, ";")
}
func (s *FolderInfo) AddTargetLabel(targetID string) {
if targetID == "" {
return
}
targetLabelsSet := s.getSetOfLabels()
targetLabelsSet[targetID] = struct{}{}
s.updateTargetLabelIDs(targetLabelsSet)
}
func (s *FolderInfo) RemoveTargetLabel(targetID string) {
if targetID == "" {
return
}
targetLabelsSet := s.getSetOfLabels()
delete(targetLabelsSet, targetID)
s.updateTargetLabelIDs(targetLabelsSet)
}
func (s *FolderInfo) IsType(askType string) bool {
return s.FolderType == askType
}
func (s *FolderInfo) getSetOfLabels() (uniqSet map[string]struct{}) {
uniqSet = make(map[string]struct{})
for _, label := range s.TargetLabelIDList() {
uniqSet[label] = struct{}{}
}
return
}
func (s *FolderInfo) TargetLabelIDList() []string {
return strings.FieldsFunc(
s.TargetLabelIDs,
func(c rune) bool { return c == ';' },
)
}
// Get data
func (s *FolderStructure) data(index *core.QModelIndex, role int) *core.QVariant {
row, isValid := index.Row(), index.IsValid()
if !isValid || row >= s.getCount() {
log.Warnln("Wrong index", isValid, row)
return core.NewQVariant()
}
var f = s.get(row)
switch role {
case FolderId:
return qtcommon.NewQVariantString(f.mailbox.Hash())
case FolderName, int(core.Qt__DisplayRole):
return qtcommon.NewQVariantString(f.mailbox.Name)
case FolderColor:
return qtcommon.NewQVariantString(f.mailbox.Color)
case FolderType:
return qtcommon.NewQVariantString(f.FolderType)
case FolderEntries:
return qtcommon.NewQVariantInt(f.FolderEntries)
case FolderFromDate:
return qtcommon.NewQVariantLong(f.FromDate)
case FolderToDate:
return qtcommon.NewQVariantLong(f.ToDate)
case IsFolderSelected:
return qtcommon.NewQVariantBool(f.IsFolderSelected)
case TargetFolderID:
return qtcommon.NewQVariantString(f.TargetFolderID)
case TargetLabelIDs:
return qtcommon.NewQVariantString(f.TargetLabelIDs)
default:
log.Warnln("Wrong role", role)
return core.NewQVariant()
}
}
// Get header data (table view, tree view)
func (s *FolderStructure) headerData(section int, orientation core.Qt__Orientation, role int) *core.QVariant {
if role != int(core.Qt__DisplayRole) {
return core.NewQVariant()
}
if orientation == core.Qt__Horizontal {
return qtcommon.NewQVariantString("Column")
}
return qtcommon.NewQVariantString("Row")
}
// Flags is editable
func (s *FolderStructure) flags(index *core.QModelIndex) core.Qt__ItemFlag {
if !index.IsValid() {
return core.Qt__ItemIsEnabled
}
// can do here also: core.NewQAbstractItemModelFromPointer(s.Pointer()).Flags(index) | core.Qt__ItemIsEditable
// or s.FlagsDefault(index) | core.Qt__ItemIsEditable
return core.Qt__ItemIsEnabled | core.Qt__ItemIsSelectable | core.Qt__ItemIsEditable
}
// Set data
func (s *FolderStructure) setData(index *core.QModelIndex, value *core.QVariant, role int) bool {
log.Debugf("SET DATA %d", role)
if !index.IsValid() {
return false
}
if index.Row() < GlobalOptionIndex || index.Row() > s.getCount() || index.Column() != 1 {
return false
}
item := s.get(index.Row())
t := true
switch role {
case FolderId, FolderType:
log.
WithField("structure", s).
WithField("row", index.Row()).
WithField("column", index.Column()).
WithField("role", role).
WithField("isEdit", role == int(core.Qt__EditRole)).
Warn("Set constant role forbiden")
case FolderName:
item.mailbox.Name = value.ToString()
case FolderColor:
item.mailbox.Color = value.ToString()
case FolderEntries:
item.FolderEntries = value.ToInt(&t)
case FolderFromDate:
item.FromDate = value.ToLongLong(&t)
case FolderToDate:
item.ToDate = value.ToLongLong(&t)
case IsFolderSelected:
item.IsFolderSelected = value.ToBool()
case TargetFolderID:
item.TargetFolderID = value.ToString()
case TargetLabelIDs:
item.TargetLabelIDs = value.ToString()
default:
log.Debugln("uknown role ", s, index.Row(), index.Column(), role, role == int(core.Qt__EditRole))
return false
}
s.changedEntityRole(index.Row(), index.Row(), role)
return true
}
// Dimension of model: number of rows is equivalent to number of items in list
func (s *FolderStructure) rowCount(parent *core.QModelIndex) int {
return s.getCount()
}
func (s *FolderStructure) getCount() int {
return len(s.entities)
}
// Returns names of available item properties
func (s *FolderStructure) roleNames() map[int]*core.QByteArray {
return s.Roles()
}
// Clear removes all items in model
func (s *FolderStructure) Clear() {
s.BeginResetModel()
if s.getCount() != 0 {
s.entities = []*FolderInfo{}
}
s.GlobalOptions = FolderInfo{
mailbox: transfer.Mailbox{
Name: "=",
},
FromDate: 0,
ToDate: 0,
TargetFolderID: "",
TargetLabelIDs: "",
}
s.EndResetModel()
}
// Method connected to addEntry slot
func (s *FolderStructure) addEntry(entry *FolderInfo) {
s.insertEntry(entry, s.getCount())
}
// NewUniqId which is not in map yet.
func (s *FolderStructure) newUniqId() (name string) {
name = s.GlobalOptions.mailbox.Name
mbox := transfer.Mailbox{Name: name}
for newVal := byte(name[0]); true; newVal++ {
mbox.Name = string([]byte{newVal})
if s.getRowById(mbox.Hash()) < GlobalOptionIndex {
return
}
}
return
}
// Method connected to addEntry slot
func (s *FolderStructure) insertEntry(entry *FolderInfo, i int) {
s.BeginInsertRows(core.NewQModelIndex(), i, i)
s.entities = append(s.entities[:i], append([]*FolderInfo{entry}, s.entities[i:]...)...)
s.EndInsertRows()
// update global if conflict
if entry.mailbox.Hash() == s.GlobalOptions.mailbox.Hash() {
globalName := s.newUniqId()
s.GlobalOptions.mailbox.Name = globalName
}
}
func (s *FolderStructure) GetInfo(row int) FolderInfo {
return *s.get(row)
}
func (s *FolderStructure) changedEntityRole(rowStart int, rowEnd int, roles ...int) {
if rowStart < GlobalOptionIndex || rowEnd < GlobalOptionIndex {
return
}
if rowStart < 0 || rowStart >= s.getCount() {
rowStart = 0
}
if rowEnd < 0 || rowEnd >= s.getCount() {
rowEnd = s.getCount()
}
if rowStart > rowEnd {
tmp := rowStart
rowStart = rowEnd
rowEnd = tmp
}
indexStart := s.Index(rowStart, 0, core.NewQModelIndex())
indexEnd := s.Index(rowEnd, 0, core.NewQModelIndex())
s.updateSelection(indexStart, indexEnd, roles)
s.DataChanged(indexStart, indexEnd, roles)
}
func (s *FolderStructure) setFolderSelection(id string, toSelect bool) {
log.Debugf("set folder selection %q %b", id, toSelect)
i := s.getRowById(id)
//
info := s.get(i)
before := info.IsFolderSelected
info.IsFolderSelected = toSelect
if err := s.saveRule(info); err != nil {
s.get(i).IsFolderSelected = before
log.WithError(err).WithField("id", id).WithField("toSelect", toSelect).Error("Cannot set selection")
return
}
//
s.changedEntityRole(i, i, IsFolderSelected)
}
func (s *FolderStructure) setTargetFolderID(id, target string) {
log.Debugf("set targetFolderID %q %q", id, target)
i := s.getRowById(id)
//
info := s.get(i)
//s.get(i).TargetFolderID = target
before := info.TargetFolderID
info.TargetFolderID = target
if err := s.saveRule(info); err != nil {
info.TargetFolderID = before
log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
return
}
//
s.changedEntityRole(i, i, TargetFolderID)
if target == "" { // do not import
before := info.TargetLabelIDs
info.TargetLabelIDs = ""
if err := s.saveRule(info); err != nil {
info.TargetLabelIDs = before
log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
return
}
s.changedEntityRole(i, i, TargetLabelIDs)
}
}
func (s *FolderStructure) addTargetLabelID(id, label string) {
log.Debugf("add target label id %q %q", id, label)
if label == "" {
return
}
i := s.getRowById(id)
info := s.get(i)
before := info.TargetLabelIDs
info.AddTargetLabel(label)
if err := s.saveRule(info); err != nil {
info.TargetLabelIDs = before
log.WithError(err).WithField("id", id).WithField("label", label).Error("Cannot add label")
return
}
s.changedEntityRole(i, i, TargetLabelIDs)
}
func (s *FolderStructure) removeTargetLabelID(id, label string) {
log.Debugf("remove label id %q %q", id, label)
if label == "" {
return
}
i := s.getRowById(id)
info := s.get(i)
before := info.TargetLabelIDs
info.RemoveTargetLabel(label)
if err := s.saveRule(info); err != nil {
info.TargetLabelIDs = before
log.WithError(err).WithField("id", id).WithField("label", label).Error("Cannot remove label")
return
}
s.changedEntityRole(i, i, TargetLabelIDs)
}
func (s *FolderStructure) setFromToDate(id string, from, to int64) {
log.Debugf("set from to date %q %d %d", id, from, to)
i := s.getRowById(id)
info := s.get(i)
beforeFrom := info.FromDate
beforeTo := info.ToDate
info.FromDate = from
info.ToDate = to
if err := s.saveRule(info); err != nil {
info.FromDate = beforeFrom
info.ToDate = beforeTo
log.WithError(err).WithField("id", id).WithField("from", from).WithField("to", to).Error("Cannot set date")
return
}
s.changedEntityRole(i, i, FolderFromDate, FolderToDate)
}
func (s *FolderStructure) selectType(folderType string, toSelect bool) {
log.Debugf("set type %q %b", folderType, toSelect)
iFirst, iLast := -1, -1
for i, entity := range s.entities {
if entity.IsType(folderType) {
if iFirst == -1 {
iFirst = i
}
before := entity.IsFolderSelected
entity.IsFolderSelected = toSelect
if err := s.saveRule(entity); err != nil {
entity.IsFolderSelected = before
log.WithError(err).WithField("i", i).WithField("type", folderType).WithField("toSelect", toSelect).Error("Cannot select type")
}
iLast = i
}
}
if iFirst != -1 {
s.changedEntityRole(iFirst, iLast, IsFolderSelected)
}
}
func (s *FolderStructure) updateSelection(topLeft *core.QModelIndex, bottomRight *core.QModelIndex, roles []int) {
for _, role := range roles {
switch role {
case IsFolderSelected:
s.SetSelectedFolders(true)
s.SetSelectedLabels(true)
s.SetAtLeastOneSelected(false)
for _, entity := range s.entities {
if entity.IsFolderSelected {
s.SetAtLeastOneSelected(true)
} else {
if entity.IsType(FolderTypeFolder) {
s.SetSelectedFolders(false)
}
if entity.IsType(FolderTypeLabel) {
s.SetSelectedLabels(false)
}
}
if !s.IsSelectedFolders() && !s.IsSelectedLabels() && s.IsAtLeastOneSelected() {
break
}
}
default:
}
}
}
func (s *FolderStructure) hasFolderWithName(name string) bool {
for _, entity := range s.entities {
if entity.mailbox.Name == name {
return true
}
}
return false
}
func (s *FolderStructure) getRowById(id string) (row int) {
for row = GlobalOptionIndex; row < s.getCount(); row++ {
if id == s.get(row).mailbox.Hash() {
return
}
}
row = GlobalOptionIndex - 1
return
}
func (s *FolderStructure) hasTarget() bool {
for row := 0; row < s.getCount(); row++ {
if s.get(row).TargetFolderID != "" {
return true
}
}
return false
}
// Getter for account info pointer
// index out of array length returns empty folder info to avoid segfault
// index == GlobalOptionIndex is set to access global options
func (s *FolderStructure) get(index int) *FolderInfo {
if index < GlobalOptionIndex || index >= s.getCount() {
return &FolderInfo{}
}
if index == GlobalOptionIndex {
return &s.GlobalOptions
}
return s.entities[index]
}

View File

@ -0,0 +1,196 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtie
// TODO:
// Proposal for new structure
// It will be a bit more memory but much better performance
// * Rules:
// * rules []Rule /QAbstracItemModel/
// * globalFromDate int64
// * globalToDate int64
// * globalLabel Mbox
// * targetPath string
// * filterEncryptedBodies bool
// * Rule
// * sourceMbox: Mbox
// * targetFolders: []Mbox /QAbstracItemModel/ (all available target folders)
// * targetLabels: []Mbox /QAbstracItemModel/ (all available target labels)
// * selectedLabelColors: QStringList (need reset context on change) (show label list)
// * fromDate int64
// * toDate int64
// * Mbox
// * IsActive bool (show checkox)
// * Name string (show name)
// * Type string (show icon)
// * Color string (show icon)
//
// Biggest update: add folder or label for all roles update target models
import (
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/therecipe/qt/core"
)
// FolderStructure model providing container for items (folder info) to QML
//
// QML ListView connects the model from Go and it shows item (entities)
// information.
//
// Copied and edited from `github.com/therecipe/qt/internal/examples/sailfish/listview`
//
// NOTE: When implementing a model it is important to remember that QAbstractItemModel does not store any data itself !!!!
// see https://doc.qt.io/qt-5/model-view-programming.html#designing-a-model
type FolderStructure struct {
core.QAbstractListModel
// QtObject Constructor
_ func() `constructor:"init"`
// List of item properties
//
// All available item properties are inside the map
_ map[int]*core.QByteArray `property:"roles"`
// The data storage
//
// The slice with all entities. It is not accessed directly but using
// `data(index,role)`
entities []*FolderInfo
GlobalOptions FolderInfo
transfer *transfer.Transfer
// Global Folders/Labels selection flag, use setter from QML
_ bool `property:"selectedLabels"`
_ bool `property:"selectedFolders"`
_ bool `property:"atLeastOneSelected"`
// Getters (const)
_ func() int `slot:"getCount"`
_ func(index int) string `slot:"getID"`
_ func(id string) string `slot:"getName"`
_ func(id string) string `slot:"getType"`
_ func(id string) string `slot:"getColor"`
_ func(id string) int64 `slot:"getFrom"`
_ func(id string) int64 `slot:"getTo"`
_ func(id string) string `slot:"getTargetLabelIDs"`
_ func(name string) bool `slot:"hasFolderWithName"`
_ func() bool `slot:"hasTarget"`
// TODO get folders
// TODO get labels
// TODO get selected labels
// TODO get selected folder
// Setters (emits DataChanged)
_ func(fileType string, toSelect bool) `slot:"selectType"`
_ func(id string, toSelect bool) `slot:"setFolderSelection"`
_ func(id string, target string) `slot:"setTargetFolderID"`
_ func(id string, label string) `slot:"addTargetLabelID"`
_ func(id string, label string) `slot:"removeTargetLabelID"`
_ func(id string, from, to int64) `slot:"setFromToDate"`
}
// FolderInfo is the element of model
//
// It contains all data for one structure entry
type FolderInfo struct {
/*
FolderId string
FolderFullPath string
FolderColor string
FolderFullName string
*/
mailbox transfer.Mailbox // TODO how to reference from qml source mailbox to go target mailbox
FolderType string
FolderEntries int // todo remove
IsFolderSelected bool
FromDate int64 // Unix seconds
ToDate int64 // Unix seconds
TargetFolderID string // target ID TODO: this will be hash
TargetLabelIDs string // semicolon separated list of label ID same here
}
// Registration of new metatype before creating instance
//
// NOTE: check it is run once per program. write a log
func init() {
FolderStructure_QRegisterMetaType()
}
// Constructor
//
// Creates the map for item properties and connects the methods
func (s *FolderStructure) init() {
s.SetRoles(map[int]*core.QByteArray{
FolderId: qtcommon.NewQByteArrayFromString("folderId"),
FolderName: qtcommon.NewQByteArrayFromString("folderName"),
FolderColor: qtcommon.NewQByteArrayFromString("folderColor"),
FolderType: qtcommon.NewQByteArrayFromString("folderType"),
FolderEntries: qtcommon.NewQByteArrayFromString("folderEntries"),
IsFolderSelected: qtcommon.NewQByteArrayFromString("isFolderSelected"),
FolderFromDate: qtcommon.NewQByteArrayFromString("fromDate"),
FolderToDate: qtcommon.NewQByteArrayFromString("toDate"),
TargetFolderID: qtcommon.NewQByteArrayFromString("targetFolderID"),
TargetLabelIDs: qtcommon.NewQByteArrayFromString("targetLabelIDs"),
})
// basic QAbstractListModel mehods
s.ConnectGetCount(s.getCount)
s.ConnectRowCount(s.rowCount)
s.ConnectColumnCount(func(parent *core.QModelIndex) int { return 1 }) // for list it should be always 1
s.ConnectData(s.data)
s.ConnectHeaderData(s.headerData)
s.ConnectRoleNames(s.roleNames)
// Editable QAbstractListModel needs: https://doc.qt.io/qt-5/model-view-programming.html#an-editable-model
s.ConnectSetData(s.setData)
s.ConnectFlags(s.flags)
// Custom FolderStructure slots to export
// Getters (const)
s.ConnectGetID(func(row int) string { return s.get(row).mailbox.Hash() })
s.ConnectGetType(func(id string) string { row := s.getRowById(id); return s.get(row).FolderType })
s.ConnectGetName(func(id string) string { row := s.getRowById(id); return s.get(row).mailbox.Name })
s.ConnectGetColor(func(id string) string { row := s.getRowById(id); return s.get(row).mailbox.Color })
s.ConnectGetFrom(func(id string) int64 { row := s.getRowById(id); return s.get(row).FromDate })
s.ConnectGetTo(func(id string) int64 { row := s.getRowById(id); return s.get(row).ToDate })
s.ConnectGetTargetLabelIDs(func(id string) string { row := s.getRowById(id); return s.get(row).TargetLabelIDs })
s.ConnectHasFolderWithName(s.hasFolderWithName)
s.ConnectHasTarget(s.hasTarget)
// Setters (emits DataChanged)
s.ConnectSelectType(s.selectType)
s.ConnectSetFolderSelection(s.setFolderSelection)
s.ConnectSetTargetFolderID(s.setTargetFolderID)
s.ConnectAddTargetLabelID(s.addTargetLabelID)
s.ConnectRemoveTargetLabelID(s.removeTargetLabelID)
s.ConnectSetFromToDate(s.setFromToDate)
s.GlobalOptions = FolderInfo{
mailbox: transfer.Mailbox{Name: "="},
FromDate: 0,
ToDate: 0,
TargetFolderID: "",
TargetLabelIDs: "",
}
}

Some files were not shown because too many files have changed in this diff Show More