forked from Silverfish/proton-bridge
Compare commits
326 Commits
v1.2.8-bet
...
v1.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cbc11a75d | |||
| a21bb130e1 | |||
| 12403785af | |||
| b4892855d4 | |||
| 7ff67f2217 | |||
| b12873f1df | |||
| ec73170e9b | |||
| 51c8bceed8 | |||
| e02c7c7f06 | |||
| 15c1d7bc24 | |||
| a89a3f6612 | |||
| d956b04062 | |||
| ef1671d4ab | |||
| fe926cbd57 | |||
| e01747e3b9 | |||
| 85220848d0 | |||
| 70f91ae55b | |||
| a73b30ed9e | |||
| 7337f78d4a | |||
| 9b5da91f7c | |||
| c7669b950f | |||
| b3ed8d51a7 | |||
| 60b7d980f4 | |||
| abf2238e6f | |||
| b4a358c084 | |||
| 3606a0ab9f | |||
| c5665d0dd7 | |||
| d6464c0048 | |||
| 5496a26f73 | |||
| ec9a799fe9 | |||
| 730abadfc3 | |||
| 60e1548685 | |||
| 7430c7f1f5 | |||
| 6671b78799 | |||
| c7578cf53c | |||
| 66e04dd5ed | |||
| 803353e300 | |||
| f3773c9d78 | |||
| 41ac61bbe8 | |||
| 0d3d6747ac | |||
| eaa9a458c4 | |||
| 46e5cb9c83 | |||
| dc5387a512 | |||
| 4b7c234e78 | |||
| 5bca6fc3cf | |||
| 97b64ebb70 | |||
| 9b3cc9dc34 | |||
| afeed4a801 | |||
| dd70b30f76 | |||
| 3e8e3c912b | |||
| 5d0e3f36b4 | |||
| da751a38e3 | |||
| f9af17dd9b | |||
| f622ecf678 | |||
| 475e673b87 | |||
| 3916ddc8e4 | |||
| ef2ace0afe | |||
| b5d3737a7e | |||
| d872d77cf5 | |||
| 1f17628399 | |||
| 4ab8f7d6b5 | |||
| fa5f4acdac | |||
| 642666fa59 | |||
| a2cf5374b9 | |||
| 6a7a77fc51 | |||
| f4dfadce52 | |||
| 9ba08e5edb | |||
| 9821b5bbc2 | |||
| 5343a6fc0f | |||
| 180c6699e0 | |||
| 7d1b0d0a40 | |||
| caff73d06c | |||
| f4d073b4cf | |||
| 65d8b382d0 | |||
| 0e7e13211b | |||
| 7e1af9ff4e | |||
| 37186846db | |||
| a5a61c9428 | |||
| ea01c155da | |||
| f4374a02da | |||
| 0d4d95360f | |||
| f88071b2ca | |||
| e01a523ae3 | |||
| c6b18b45b5 | |||
| a7da66ccbc | |||
| 8bd74c5edc | |||
| 2b36d3ab7b | |||
| 45b863f931 | |||
| 953150cfdb | |||
| 6ea3fc1963 | |||
| 7207a5d59e | |||
| dd2264da6f | |||
| 9261b6337e | |||
| 4f6e8c30c7 | |||
| 614a00eac1 | |||
| de58c7a905 | |||
| 2e439e17cf | |||
| f73aeec97f | |||
| 8a7b4bb919 | |||
| 78fd73ee2a | |||
| bfdfc81d65 | |||
| bf6963859f | |||
| 33bf64cc4e | |||
| bb1d27a5be | |||
| bc07896436 | |||
| 1d2e584799 | |||
| 9218598140 | |||
| af89931f05 | |||
| 84147a2cb0 | |||
| 2269a9edb7 | |||
| 61867fbde7 | |||
| 2d9417d501 | |||
| 4973e38748 | |||
| e4704cd459 | |||
| 8592a264c0 | |||
| 40aeb6c010 | |||
| 71b9a3b205 | |||
| 2182e573f9 | |||
| 5f02e59fa4 | |||
| 29ff8cf54b | |||
| f8cf4e966f | |||
| df80e7eb27 | |||
| 658ead9fb3 | |||
| 4f0af0fb02 | |||
| 7e5e3d3dd4 | |||
| 1c10cc5065 | |||
| 49316a935c | |||
| b598779c0f | |||
| 9d65192ad7 | |||
| 0e14155185 | |||
| 56f4f3d017 | |||
| f5617ced3f | |||
| 35b37c7097 | |||
| 77c6ba381e | |||
| 34df24ede3 | |||
| 33d705a39d | |||
| 64fbd0655f | |||
| b700a7823e | |||
| 145da7ffa5 | |||
| 61a841ced7 | |||
| 29978b7014 | |||
| dd73687555 | |||
| 5411b29d17 | |||
| 6c93f1f1ec | |||
| 1dcaa200e0 | |||
| 66082af40f | |||
| 209af59232 | |||
| 9f24c666b9 | |||
| 3101fc5543 | |||
| 182bbd556f | |||
| e333ccd29e | |||
| ce4a75caf5 | |||
| 01a8c9e9d7 | |||
| 2c910378ce | |||
| 34ef9063cb | |||
| f651d39820 | |||
| 7e6d09a247 | |||
| da381130a3 | |||
| 7baa4dc117 | |||
| be07cb83c9 | |||
| dfbd86c7bc | |||
| e3ab829ad3 | |||
| b12ef1327c | |||
| d66bcc4b63 | |||
| 5ad307868e | |||
| 369c6ebf85 | |||
| c988d739a1 | |||
| 36ef9f20ae | |||
| c8f118a26b | |||
| be20714842 | |||
| 1711442878 | |||
| 79e6799f40 | |||
| 6023162443 | |||
| 01e0fe4863 | |||
| f073301481 | |||
| 1df81e4a34 | |||
| bf0945eaef | |||
| 11e01ca163 | |||
| a650a04a88 | |||
| bdc11c8358 | |||
| ed7a0dc9b3 | |||
| fc4e77604f | |||
| abaeace4b3 | |||
| 457b524ba8 | |||
| d89d627349 | |||
| 51ff880fd9 | |||
| b25baa2524 | |||
| 10e384f4df | |||
| 35ae2011b6 | |||
| 5348ae7d18 | |||
| 2512d3647a | |||
| 1e8cb35fcb | |||
| 0b0991d682 | |||
| 813e99f399 | |||
| b6707749e5 | |||
| 7ec4309ae1 | |||
| ec224a962f | |||
| 012be60311 | |||
| 02804d067c | |||
| 9241a9bdbf | |||
| f3e6af5571 | |||
| 7a13b89274 | |||
| 5cb78b0a03 | |||
| c19bb0fa97 | |||
| de16f6f2d1 | |||
| 7963b3c152 | |||
| f82ab3189b | |||
| 49cc49b1e2 | |||
| 9808c44714 | |||
| c329711f9c | |||
| 928fa93765 | |||
| 45e99caa23 | |||
| 80b2bfc2a5 | |||
| 6070a3b7cc | |||
| 9e633400b0 | |||
| 84d344cb0a | |||
| e43033b42b | |||
| e5d63edb62 | |||
| 579e962980 | |||
| 2919d1a3c0 | |||
| 1ba319bb69 | |||
| 8cdebb6d05 | |||
| cc14b523cb | |||
| ad877431de | |||
| 40d8c458d2 | |||
| 3b0b1a457b | |||
| 7ac4c9aecf | |||
| 390182d247 | |||
| cb8a15a9fd | |||
| 4d2baa6b85 | |||
| 4e2ab9b389 | |||
| c6c6cfc7d7 | |||
| a78b1ca00f | |||
| d718720b29 | |||
| f64cb4b56d | |||
| b2b43ac909 | |||
| f2b8d02cd2 | |||
| 3c92ff18ff | |||
| 41f6cd3bcd | |||
| 66f23bef99 | |||
| bbf1364e30 | |||
| 6147c214c3 | |||
| f87ca36ffd | |||
| 37f4e46bdc | |||
| a7b9572e6b | |||
| 4090c490b1 | |||
| d33d7237bd | |||
| 9ed778f2b3 | |||
| 70fca64a36 | |||
| 30425d5fcd | |||
| 2639f7333e | |||
| 4a8d07d54e | |||
| 833fce8702 | |||
| c1a57a2e12 | |||
| cda3000a7a | |||
| 4b2977041a | |||
| 2d200f6f8c | |||
| c61e8bdc71 | |||
| 7e8dc22837 | |||
| e43bd231ed | |||
| 1998d92432 | |||
| 313e803fdd | |||
| cabcb3ae2b | |||
| e57a3c2a3a | |||
| 7a87a7ea2f | |||
| c939893131 | |||
| ea0f3115a3 | |||
| 3d3b91b242 | |||
| cd38c86b4b | |||
| 2379598078 | |||
| 984b28e8f9 | |||
| 1d49a484a8 | |||
| 99aabf07b3 | |||
| 6e537db5ff | |||
| 668fc7f039 | |||
| 284a097d4f | |||
| e5944518ca | |||
| df3a9ea19e | |||
| 2db1b113e0 | |||
| 68d2591c73 | |||
| e9735c6110 | |||
| 0fd5ca3a24 | |||
| 8c2f88fe70 | |||
| 23f492705b | |||
| 44233e5bd3 | |||
| 33770ce129 | |||
| faec347054 | |||
| 9b68625522 | |||
| d42deb2ad5 | |||
| bb5227c1f4 | |||
| 0589f329e9 | |||
| 522cadb8b1 | |||
| 32ca7b3903 | |||
| 7d30459417 | |||
| 8f15041d8f | |||
| 51846efed5 | |||
| a51841158c | |||
| 1457005f86 | |||
| febdf98349 | |||
| d4482994ec | |||
| 244a18ac8c | |||
| e027aa5fae | |||
| 99635cd56d | |||
| e95aece6d3 | |||
| 38f0425670 | |||
| 4809d97cb1 | |||
| bfc4069df4 | |||
| 3f32fd95e0 | |||
| 40e96b9d1e | |||
| 80f4e1e346 | |||
| debd374d75 | |||
| ed8595fa5b | |||
| fec5f2d3c3 | |||
| bafd4e714e | |||
| d787d8b223 | |||
| abca7284dd | |||
| db02eb694d | |||
| 5bf4d9c6f5 | |||
| b01be382fc | |||
| 042c340881 | |||
| f269be4291 | |||
| 6e38a65bd8 | |||
| 941e09079c | |||
| ce29d4d74e | |||
| f239e8f3bf | |||
| 0a55fac29a |
30
.gitignore
vendored
30
.gitignore
vendored
@ -18,8 +18,30 @@ coverage.html
|
|||||||
mem.pprof
|
mem.pprof
|
||||||
|
|
||||||
# Auto generated frontend
|
# Auto generated frontend
|
||||||
frontend/qml/BridgeUI/*.qmlc
|
internal/frontend/qml/BridgeUI/*.qmlc
|
||||||
frontend/qml/ProtonUI/*.qmlc
|
internal/frontend/qml/ImportExportUI/*.qmlc
|
||||||
frontend/qml/ProtonUI/fontawesome.ttf
|
internal/frontend/qml/ProtonUI/*.qmlc
|
||||||
frontend/qml/ProtonUI/images
|
internal/frontend/qml/ProtonUI/fontawesome.ttf
|
||||||
|
internal/frontend/qml/ProtonUI/images
|
||||||
|
internal/frontend/qml/ImportExportUI/images
|
||||||
frontend/qml/*.qmlc
|
frontend/qml/*.qmlc
|
||||||
|
|
||||||
|
# Build files
|
||||||
|
bridge_darwin_*.tgz
|
||||||
|
cmd/Desktop-Bridge/deploy
|
||||||
|
cmd/Import-Export/deploy
|
||||||
|
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_*.go
|
||||||
|
vendor-cache/
|
||||||
|
|
||||||
|
/main.go
|
||||||
|
|||||||
162
.gitlab-ci.yml
162
.gitlab-ci.yml
@ -1,4 +1,4 @@
|
|||||||
image: gitlab.protontech.ch:4567/go/bridge/ci
|
image: gitlab.protontech.ch:4567/go/bridge-internal
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- eval $(ssh-agent -s)
|
- eval $(ssh-agent -s)
|
||||||
@ -13,38 +13,15 @@ before_script:
|
|||||||
cache:
|
cache:
|
||||||
key: go-mod
|
key: go-mod
|
||||||
paths:
|
paths:
|
||||||
- .cache
|
- .cache
|
||||||
policy: pull
|
policy: pull
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- image
|
|
||||||
- cache
|
- cache
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
- mirror
|
- 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
|
# Stage: CACHE
|
||||||
|
|
||||||
# This will ensure latest dependency versions and updates the cache for
|
# This will ensure latest dependency versions and updates the cache for
|
||||||
@ -54,11 +31,11 @@ cache-push:
|
|||||||
only:
|
only:
|
||||||
- branches
|
- branches
|
||||||
script:
|
script:
|
||||||
- echo ""
|
- echo ""
|
||||||
cache:
|
cache:
|
||||||
key: go-mod
|
key: go-mod
|
||||||
paths:
|
paths:
|
||||||
- .cache
|
- .cache
|
||||||
|
|
||||||
# Stage: TEST
|
# Stage: TEST
|
||||||
|
|
||||||
@ -98,18 +75,141 @@ dependency-updates:
|
|||||||
|
|
||||||
# Stage: BUILD
|
# Stage: BUILD
|
||||||
|
|
||||||
build-linux:
|
.build-base:
|
||||||
stage: build
|
stage: build
|
||||||
# Test build every time (= we want to know build is possible).
|
|
||||||
only:
|
only:
|
||||||
- branches
|
- branches
|
||||||
script:
|
script:
|
||||||
- make build
|
- make build
|
||||||
artifacts:
|
artifacts:
|
||||||
name: "bridge-linux-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA"
|
expire_in: 2 week
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
extends: .build-base
|
||||||
|
artifacts:
|
||||||
|
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
|
||||||
paths:
|
paths:
|
||||||
- bridge_*.tgz
|
- bridge_*.tgz
|
||||||
expire_in: 2 week
|
|
||||||
|
build-linux-qa:
|
||||||
|
extends: .build-base
|
||||||
|
only:
|
||||||
|
- web
|
||||||
|
script:
|
||||||
|
- BUILD_TAGS="build_qa pmapi_qa" make build
|
||||||
|
artifacts:
|
||||||
|
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
|
||||||
|
paths:
|
||||||
|
- bridge_*.tgz
|
||||||
|
|
||||||
|
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
|
||||||
|
- export PATH=/usr/local/bin:$PATH
|
||||||
|
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||||
|
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||||
|
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
|
||||||
|
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||||
|
- export GOPATH=~/go
|
||||||
|
- export PATH=$GOPATH/bin:$PATH
|
||||||
|
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
||||||
|
cache: {}
|
||||||
|
tags:
|
||||||
|
- macOS
|
||||||
|
|
||||||
|
build-darwin:
|
||||||
|
extends: .build-darwin-base
|
||||||
|
artifacts:
|
||||||
|
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
|
||||||
|
paths:
|
||||||
|
- bridge_*.tgz
|
||||||
|
|
||||||
|
build-darwin-qa:
|
||||||
|
extends: .build-darwin-base
|
||||||
|
only:
|
||||||
|
- web
|
||||||
|
script:
|
||||||
|
- BUILD_TAGS="build_qa pmapi_qa" make build
|
||||||
|
artifacts:
|
||||||
|
name: "bridge-darwin-qa-$CI_COMMIT_SHORT_SHA"
|
||||||
|
paths:
|
||||||
|
- bridge_*.tgz
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
- 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
|
||||||
|
artifacts:
|
||||||
|
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
|
||||||
|
paths:
|
||||||
|
- bridge_*.tgz
|
||||||
|
|
||||||
|
build-windows-qa:
|
||||||
|
extends: .build-windows-base
|
||||||
|
only:
|
||||||
|
- web
|
||||||
|
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 BUILD_TAGS="build_qa pmapi_qa" make build
|
||||||
|
artifacts:
|
||||||
|
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||||
|
paths:
|
||||||
|
- bridge_*.tgz
|
||||||
|
|
||||||
|
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:
|
mirror-repo:
|
||||||
stage: mirror
|
stage: mirror
|
||||||
|
|||||||
@ -21,6 +21,12 @@ issues:
|
|||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gosec
|
- gosec
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
godox:
|
||||||
|
keywords:
|
||||||
|
- TODO
|
||||||
|
- FIXME
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# setting disable-all will make only explicitly enabled linters run
|
# setting disable-all will make only explicitly enabled linters run
|
||||||
disable-all: true
|
disable-all: true
|
||||||
|
|||||||
24
BUILDS.md
24
BUILDS.md
@ -1,4 +1,4 @@
|
|||||||
# Building ProtonMail Bridge app
|
# Building ProtonMail Bridge and Import-Export app
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
* Go 1.13
|
* Go 1.13
|
||||||
@ -6,6 +6,7 @@
|
|||||||
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||||
* GCC (linux, windows) or Xcode (macOS)
|
* GCC (linux, windows) or Xcode (macOS)
|
||||||
* Windres (windows)
|
* Windres (windows)
|
||||||
|
* libglvnd and libsecret development files (linux)
|
||||||
|
|
||||||
To enable the sending of crash reports using Sentry please set the
|
To enable the sending of crash reports using Sentry please set the
|
||||||
`main.DSNSentry` value with the client key of your sentry project before build.
|
`main.DSNSentry` value with the client key of your sentry project before build.
|
||||||
@ -18,6 +19,8 @@ Otherwise, the sending of crash reports will be disabled.
|
|||||||
export MSYSTEM=
|
export MSYSTEM=
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Build Bridge
|
||||||
* in project root run
|
* in project root run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -25,9 +28,22 @@ make build
|
|||||||
```
|
```
|
||||||
|
|
||||||
* The result will be stored in `./cmd/Destop-Bridge/deploy/${GOOS}/`
|
* 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 `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 `bridge.exe`)
|
* 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 `bridge.app`)
|
* 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
|
## Useful tests, lints and checks
|
||||||
In order to be able to run following commands please install the development dependencies:
|
In order to be able to run following commands please install the development dependencies:
|
||||||
|
|||||||
84
COPYING.md
84
COPYING.md
@ -26,48 +26,48 @@ ProtonMail Bridge includes the following 3rd party software:
|
|||||||
* [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing)
|
* [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing)
|
||||||
* [Font Awesome 4.7.0](https://fontawesome.com/v4.7.0/) | Available under [multiple licenses](https://fontawesome.com/v4.7.0/license/)
|
* [Font Awesome 4.7.0](https://fontawesome.com/v4.7.0/) | Available under [multiple licenses](https://fontawesome.com/v4.7.0/license/)
|
||||||
|
|
||||||
* [notificator](github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
|
* [notificator](https://github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
|
||||||
* [ishell](github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
|
* [ishell](https://github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
|
||||||
* [readline](github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
* [readline](https://github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||||
* [singleinstance](github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
|
* [singleinstance](https://github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
|
||||||
* [cascadia](github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
* [cascadia](https://github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
||||||
* [gocertifi](github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE)
|
* [gocertifi](https://github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE)
|
||||||
* [logex](github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
|
* [logex](https://github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
|
||||||
* [test](github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
* [test](https://github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||||
* [godog](github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
|
* [godog](https://github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
|
||||||
* [wincred](github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
|
* [wincred](https://github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
|
||||||
* [credential-helpers](github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
|
* [credential-helpers](https://github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
|
||||||
* [imap](github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
|
* [imap](https://github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
|
||||||
* [imap-appendlimit](github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
|
* [imap-appendlimit](https://github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
|
||||||
* [imap-idle](github.com/emersion/go-imap-idle) | Available under [license](https://github.com/emersion/go-imap-idle/blob/master/LICENSE)
|
* [imap-idle](https://github.com/emersion/go-imap-idle) | Available under [license](https://github.com/emersion/go-imap-idle/blob/master/LICENSE)
|
||||||
* [imap-quota](github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
|
* [imap-quota](https://github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
|
||||||
* [imap-specialuse](github.com/emersion/go-imap-specialuse) | Available under [license](https://github.com/emersion/go-imap-specialuse/blob/master/LICENSE)
|
* [imap-specialuse](https://github.com/emersion/go-imap-specialuse) | Available under [license](https://github.com/emersion/go-imap-specialuse/blob/master/LICENSE)
|
||||||
* [sasl](github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
* [sasl](https://github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
||||||
* [smtp](github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
* [smtp](https://github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
||||||
* [textwrapper](github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
* [textwrapper](https://github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||||
* [vcard](github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
* [vcard](https://github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||||
* [color](github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
* [color](https://github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE.md)
|
||||||
* [shlex](github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
* [shlex](https://github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/COPYING)
|
||||||
* [raven](github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE)
|
* [raven](https://github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE)
|
||||||
* [resty](github.com/go-resty/resty/v2) | Available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
* [resty](https://github.com/go-resty/resty) | Available under [license](https://github.com/go-resty/resty/blob/master/LICENSE)
|
||||||
* [mock](github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE)
|
* [mock](https://github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE)
|
||||||
* [cmp](github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
|
* [cmp](https://github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
|
||||||
* [gopherjs](github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE)
|
* [gopherjs](https://github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE)
|
||||||
* [multierror](github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
|
* [multierror](https://github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
|
||||||
* [bcrypt](github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
|
* [bcrypt](https://github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
|
||||||
* [html2text](github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
* [html2text](https://github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
||||||
* [enmime](github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE)
|
* [enmime](https://github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE)
|
||||||
* [osext](github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE)
|
* [osext](https://github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE)
|
||||||
* [keychain](github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
* [keychain](https://github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
||||||
* [aurora](github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
|
* [aurora](https://github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
|
||||||
* [dns](github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
* [dns](https://github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
||||||
* [uuid](github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE)
|
* [uuid](https://github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE)
|
||||||
* [jsondiff](github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
|
* [jsondiff](https://github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
|
||||||
* [logrus](github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
|
* [logrus](https://github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
|
||||||
* [golang](github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE)
|
* [golang](https://github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE)
|
||||||
* [testify](github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
* [testify](https://github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||||
* [uuid](github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE)
|
* [uuid](https://github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE)
|
||||||
* [cli](github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE)
|
* [cli](https://github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE)
|
||||||
|
|
||||||
* [BBolt](https://pkg.go.dev/go.etcd.io/bbolt/?tab=doc) | Available under [license](https://pkg.go.dev/go.etcd.io/bbolt?tab=licenses#LICENSE)
|
* [BBolt](https://pkg.go.dev/go.etcd.io/bbolt/?tab=doc) | Available under [license](https://pkg.go.dev/go.etcd.io/bbolt?tab=licenses#LICENSE)
|
||||||
* [testify.v1](https://gopkg.in/stretchr/testify.v1) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
* [testify.v1](https://gopkg.in/stretchr/testify.v1) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||||
|
|||||||
1258
Changelog.md
1258
Changelog.md
File diff suppressed because it is too large
Load Diff
178
Makefile
178
Makefile
@ -1,17 +1,36 @@
|
|||||||
export GO111MODULE=on
|
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)
|
GOOS:=$(shell go env GOOS)
|
||||||
|
TARGET_CMD?=Desktop-Bridge
|
||||||
|
TARGET_OS?=${GOOS}
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
.PHONY: build build-nogui check-has-go
|
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
|
||||||
|
|
||||||
VERSION?=1.2.7-git
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
|
BRIDGE_APP_VERSION?=1.4.0-git
|
||||||
|
IE_APP_VERSION?=1.1.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)
|
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||||
|
|
||||||
BUILD_TAGS?=pmapi_prod
|
BUILD_TAGS?=pmapi_prod
|
||||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||||
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
|
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
|
||||||
GO_LDFLAGS:=$(addprefix -X main.,Version=${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}" ""
|
ifneq "${BUILD_LDFLAGS}" ""
|
||||||
GO_LDFLAGS+= ${BUILD_LDFLAGS}
|
GO_LDFLAGS+= ${BUILD_LDFLAGS}
|
||||||
endif
|
endif
|
||||||
@ -19,78 +38,95 @@ GO_LDFLAGS:=-ldflags '${GO_LDFLAGS}'
|
|||||||
BUILD_FLAGS+= ${GO_LDFLAGS}
|
BUILD_FLAGS+= ${GO_LDFLAGS}
|
||||||
BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
|
BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
|
||||||
|
|
||||||
DEPLOY_DIR:=cmd/Desktop-Bridge/deploy
|
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||||
ICO_FILES:=
|
ICO_FILES:=
|
||||||
EXE:=$(shell basename ${CURDIR})
|
EXE:=$(shell basename ${CURDIR})
|
||||||
|
|
||||||
ifeq "${GOOS}" "windows"
|
ifeq "${TARGET_OS}" "windows"
|
||||||
EXE+=.exe
|
EXE:=${EXE}.exe
|
||||||
ICO_FILES:=logo.ico icon.rc icon_windows.syso
|
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
|
||||||
endif
|
endif
|
||||||
ifeq "${GOOS}" "darwin"
|
ifeq "${TARGET_OS}" "darwin"
|
||||||
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
||||||
EXE:=${EXE}.app/Contents/MacOS/${EXE}
|
EXE:=${EXE}.app/Contents/MacOS/${EXE}
|
||||||
endif
|
endif
|
||||||
EXE_TARGET:=${DEPLOY_DIR}/${GOOS}/${EXE}
|
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
|
||||||
TGZ_TARGET:=bridge_${GOOS}_${REVISION}.tgz
|
|
||||||
|
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
|
||||||
|
ifeq "${TARGET_CMD}" "Import-Export"
|
||||||
|
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
build: ${TGZ_TARGET}
|
build: ${TGZ_TARGET}
|
||||||
|
build-ie:
|
||||||
|
TARGET_CMD=Import-Export $(MAKE) build
|
||||||
|
|
||||||
build-nogui:
|
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
|
||||||
|
|
||||||
${TGZ_TARGET}: ${DEPLOY_DIR}/${GOOS}
|
build-ie-nogui:
|
||||||
|
TARGET_CMD=Import-Export $(MAKE) build-nogui
|
||||||
|
|
||||||
|
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
|
||||||
rm -f $@
|
rm -f $@
|
||||||
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${GOOS}
|
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${TARGET_OS}
|
||||||
|
|
||||||
${DEPLOY_DIR}/linux: ${EXE_TARGET}
|
${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 ./LICENSE ${DEPLOY_DIR}/linux/
|
||||||
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
|
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
|
||||||
|
|
||||||
${DEPLOY_DIR}/darwin: ${EXE_TARGET}
|
${DEPLOY_DIR}/darwin: ${EXE_TARGET}
|
||||||
cp ./internal/frontend/share/icons/Bridge.icns ${DARWINAPP_CONTENTS}/Resources/
|
cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${TGT_ICNS}
|
||||||
cp -r "utils/addcert.scpt" ${DARWINAPP_CONTENTS}/Resources/
|
|
||||||
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
|
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
|
||||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
|
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
|
||||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
|
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
|
||||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
|
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
|
||||||
|
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}"
|
||||||
|
|
||||||
${DEPLOY_DIR}/windows: ${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/
|
cp LICENSE ${DEPLOY_DIR}/windows/
|
||||||
|
|
||||||
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
|
QT_BUILD_TARGET:=build desktop
|
||||||
rm -rf deploy ${GOOS} ${DEPLOY_DIR}
|
ifneq "${GOOS}" "${TARGET_OS}"
|
||||||
cp cmd/Desktop-Bridge/main.go .
|
ifeq "${TARGET_OS}" "windows"
|
||||||
qtdeploy ${BUILD_FLAGS} build desktop
|
QT_BUILD_TARGET:=-docker build windows_64_shared
|
||||||
mv deploy cmd/Desktop-Bridge
|
endif
|
||||||
rm -rf ${GOOS} main.go
|
endif
|
||||||
|
|
||||||
logo.ico: ./internal/frontend/share/icons/logo.ico
|
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
|
||||||
cp $^ .
|
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
|
||||||
|
cp cmd/${TARGET_CMD}/main.go .
|
||||||
|
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
|
||||||
|
mv deploy cmd/${TARGET_CMD}
|
||||||
|
rm -rf ${TARGET_OS} main.go
|
||||||
|
|
||||||
|
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
|
||||||
|
cp $^ $@
|
||||||
icon.rc: ./internal/frontend/share/icon.rc
|
icon.rc: ./internal/frontend/share/icon.rc
|
||||||
cp $^ .
|
cp $^ .
|
||||||
./internal/frontend/qt/icon_windows.syso: ./internal/frontend/share/icon.rc logo.ico
|
icon_windows.syso: icon.rc logo.ico
|
||||||
windres $< $@
|
windres --target=pe-x86-64 -o $@ $<
|
||||||
icon_windows.syso: ./internal/frontend/qt/icon_windows.syso
|
|
||||||
cp $^ .
|
|
||||||
|
|
||||||
|
|
||||||
## Rules for therecipe/qt
|
## Rules for therecipe/qt
|
||||||
.PHONY: prepare-vendor update-vendor
|
.PHONY: prepare-vendor update-vendor
|
||||||
THERECIPE_QTVER:=$(shell grep "github.com/therecipe/qt " go.mod | sed -r 's;.* v[0-9\.]+-[0-9]+-([a-f0-9]*).*;\1;')
|
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
|
||||||
THERECIPE_ENV:=github.com/therecipe/env_${GOOS}_amd64_513
|
|
||||||
|
|
||||||
# vendor folder will be deleted by gomod hence we cache the big repo
|
# vendor folder will be deleted by gomod hence we cache the big repo
|
||||||
# therecipe/env in order to download it only once
|
# therecipe/env in order to download it only once
|
||||||
vendor-cache/${THERECIPE_ENV}:
|
vendor-cache/${THERECIPE_ENV}:
|
||||||
git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV}
|
git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV}
|
||||||
|
|
||||||
|
# The command used to make symlinks is different on windows.
|
||||||
|
# So if the GOOS is windows and we aren't crossbuilding (in which case the host os would still be *nix)
|
||||||
|
# we need to change the LINKCMD to something windowsy.
|
||||||
LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV}
|
LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV}
|
||||||
ifeq "${GOOS}" "windows"
|
ifeq "${GOOS}" "windows"
|
||||||
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
|
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
|
||||||
LINKCMD:=cmd //c 'mklink $(subst /,\,vendor\${THERECIPE_ENV} ${WINDIR})'
|
LINKCMD:=cmd //c 'mklink $(subst /,\,vendor\${THERECIPE_ENV} ${WINDIR})'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
prepare-vendor:
|
prepare-vendor:
|
||||||
@ -104,7 +140,7 @@ update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
|
|||||||
|
|
||||||
## Dev dependencies
|
## Dev dependencies
|
||||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated
|
.PHONY: install-devel-tools install-linter install-go-mod-outdated
|
||||||
LINTVER:="v1.23.6"
|
LINTVER:="v1.29.0"
|
||||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||||
|
|
||||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||||
@ -122,12 +158,15 @@ install-go-mod-outdated:
|
|||||||
|
|
||||||
|
|
||||||
## Checks, mocks and docs
|
## Checks, mocks and docs
|
||||||
.PHONY: check-has-go check-license test bench coverage mocks lint updates doc
|
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc
|
||||||
check-has-go:
|
check-has-go:
|
||||||
@which go || (echo "Install Go-lang!" && exit 1)
|
@which go || (echo "Install Go-lang!" && exit 1)
|
||||||
|
|
||||||
check-license:
|
add-license:
|
||||||
find . -not -path "./vendor/*" -not -name "*mock*.go" -regextype posix-egrep -regex ".*\.go|.*\.qml" -exec grep -L "Copyright (c) 2020 Proton Technologies AG" {} \;
|
./utils/missing_license.sh add
|
||||||
|
|
||||||
|
change-copyright-year:
|
||||||
|
./utils/missing_license.sh change-year
|
||||||
|
|
||||||
test: gofiles
|
test: gofiles
|
||||||
@# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests.
|
@# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests.
|
||||||
@ -138,9 +177,14 @@ test: gofiles
|
|||||||
./internal/frontend/autoconfig/... \
|
./internal/frontend/autoconfig/... \
|
||||||
./internal/frontend/cli/... \
|
./internal/frontend/cli/... \
|
||||||
./internal/imap/... \
|
./internal/imap/... \
|
||||||
|
./internal/metrics/... \
|
||||||
|
./internal/importexport/... \
|
||||||
./internal/preferences/... \
|
./internal/preferences/... \
|
||||||
./internal/smtp/... \
|
./internal/smtp/... \
|
||||||
./internal/store/... \
|
./internal/store/... \
|
||||||
|
./internal/transfer/... \
|
||||||
|
./internal/updates/... \
|
||||||
|
./internal/users/... \
|
||||||
./pkg/...
|
./pkg/...
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
@ -152,11 +196,18 @@ coverage: test
|
|||||||
go tool cover -html=/tmp/coverage.out -o=coverage.html
|
go tool cover -html=/tmp/coverage.out -o=coverage.html
|
||||||
|
|
||||||
mocks:
|
mocks:
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/bridge Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer > internal/bridge/mocks/mocks.go
|
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/store PanicHandler,BridgeUser > internal/store/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/listener Listener > internal/store/mocks/utils_mocks.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
|
||||||
|
|
||||||
lint:
|
lint: lint-golang lint-license
|
||||||
|
|
||||||
|
lint-license:
|
||||||
|
./utils/missing_license.sh check
|
||||||
|
|
||||||
|
lint-golang:
|
||||||
which golangci-lint || $(MAKE) install-linter
|
which golangci-lint || $(MAKE) install-linter
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
@ -170,15 +221,20 @@ doc:
|
|||||||
.PHONY: gofiles
|
.PHONY: gofiles
|
||||||
# Following files are for the whole app so it makes sense to have them in bridge package.
|
# 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.)
|
# (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
|
./internal/bridge/credits.go: ./utils/credits.sh go.mod
|
||||||
cd ./utils/ && ./credits.sh
|
cd ./utils/ && ./credits.sh bridge
|
||||||
./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes.txt ./release-notes/bugs.txt
|
./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes-bridge.txt ./release-notes/bugs-bridge.txt
|
||||||
cd ./utils/ && ./release-notes.sh
|
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
|
## 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
|
VERBOSITY?=debug-client
|
||||||
RUN_FLAGS:=-m -l=${VERBOSITY}
|
RUN_FLAGS:=-m -l=${VERBOSITY}
|
||||||
|
|
||||||
@ -190,23 +246,39 @@ run-qt-cli: ${EXE_TARGET}
|
|||||||
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
|
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
|
||||||
|
|
||||||
run-nogui: clean-vendor gofiles
|
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
|
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:
|
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:
|
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:
|
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
|
rm -rf ./vendor
|
||||||
|
|
||||||
clean: clean-frontend-qt
|
clean: clean-vendor
|
||||||
rm -rf vendor-cache
|
rm -rf vendor-cache
|
||||||
rm -rf cmd/Desktop-Bridge/deploy
|
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
|
||||||
|
|||||||
50
README.md
50
README.md
@ -1,4 +1,4 @@
|
|||||||
# ProtonMail Bridge
|
# ProtonMail Bridge and Import Export app
|
||||||
Copyright (c) 2020 Proton Technologies AG
|
Copyright (c) 2020 Proton Technologies AG
|
||||||
|
|
||||||
This repository holds the ProtonMail Bridge application.
|
This repository holds the ProtonMail Bridge application.
|
||||||
@ -6,7 +6,8 @@ For a detailed build information see [BUILDS](./BUILDS.md).
|
|||||||
For licensing information see [COPYING](./COPYING.md).
|
For licensing information see [COPYING](./COPYING.md).
|
||||||
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
|
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||||
|
|
||||||
## Description
|
|
||||||
|
## Description Bridge
|
||||||
ProtonMail Bridge for e-mail clients.
|
ProtonMail Bridge for e-mail clients.
|
||||||
|
|
||||||
When launched, Bridge will initialize local IMAP/SMTP servers and render
|
When launched, Bridge will initialize local IMAP/SMTP servers and render
|
||||||
@ -23,6 +24,16 @@ background.
|
|||||||
|
|
||||||
More details [on the public website](https://protonmail.com/bridge).
|
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
|
## Keychain
|
||||||
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
|
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
|
||||||
@ -31,22 +42,53 @@ Windows, Bridge uses native credential managers. On Linux, use
|
|||||||
or
|
or
|
||||||
[pass](https://www.passwordstore.org/).
|
[pass](https://www.passwordstore.org/).
|
||||||
|
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
### Bridge application
|
### Bridge application
|
||||||
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
|
- `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
|
### Dev build or run
|
||||||
|
- `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
|
- `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.
|
- `VERBOSITY`: set log level used during test time and by the makefile
|
||||||
- `VERSION`: set the bridge app version used during testing or building.
|
|
||||||
|
|
||||||
### Integration testing
|
### Integration testing
|
||||||
- `TEST_ENV`: set which env to use (fake or live)
|
- `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
|
- `TEST_ACCOUNTS`: set JSON file with configured accounts
|
||||||
- `TAGS`: set build tags for tests
|
- `TAGS`: set build tags for tests
|
||||||
- `FEATURES`: set feature dir, file or scenario to test
|
- `FEATURES`: set feature dir, file or scenario to test
|
||||||
|
|
||||||
|
|
||||||
|
## Files
|
||||||
|
### Database
|
||||||
|
The database stores metadata necessary for presenting messages and mailboxes to an email client:
|
||||||
|
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
||||||
|
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
|
||||||
|
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
|
||||||
|
|
||||||
|
### Preferences
|
||||||
|
User preferences are stored in json at the following location:
|
||||||
|
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/prefs.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
||||||
|
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/prefs.json`
|
||||||
|
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\prefs.json`
|
||||||
|
|
||||||
|
### IMAP Cache
|
||||||
|
The currently subscribed mailboxes are held in a json file:
|
||||||
|
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
||||||
|
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
|
||||||
|
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
|
||||||
|
|
||||||
|
### Lock file
|
||||||
|
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
|
||||||
|
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
||||||
|
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
|
||||||
|
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
|
||||||
|
|
||||||
|
### TLS Certificate and Key
|
||||||
|
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
|
||||||
|
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
|
||||||
|
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
|
||||||
|
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -35,138 +35,56 @@ package main
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
|
"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/events"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/imap"
|
"github.com/ProtonMail/proton-bridge/internal/imap"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/pmapifactory"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/smtp"
|
"github.com/ProtonMail/proton-bridge/internal/smtp"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/args"
|
"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/config"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
"github.com/allan-simon/go-singleinstance"
|
"github.com/allan-simon/go-singleinstance"
|
||||||
"github.com/getsentry/raven-go"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
|
const (
|
||||||
// Different number will drop old files and create new ones.
|
// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
|
||||||
const cacheVersion = "c11"
|
// Different number will drop old files and create new ones.
|
||||||
|
cacheVersion = "c11"
|
||||||
|
|
||||||
// Following variables are set via ldflags during build.
|
appName = "bridge"
|
||||||
var (
|
|
||||||
// Version of the build.
|
|
||||||
Version = "" //nolint[gochecknoglobals]
|
|
||||||
// Revision is current hash of the build.
|
|
||||||
Revision = "" //nolint[gochecknoglobals]
|
|
||||||
// BuildTime stamp of the build.
|
|
||||||
BuildTime = "" //nolint[gochecknoglobals]
|
|
||||||
// AppShortName to make setup
|
|
||||||
AppShortName = "bridge" //nolint[gochecknoglobals]
|
|
||||||
// DSNSentry client keys to be able to report crashes to Sentry
|
|
||||||
DSNSentry = "" //nolint[gochecknoglobals]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
longVersion = Version + " (" + Revision + ")" //nolint[gochecknoglobals]
|
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
|
||||||
buildVersion = longVersion + " " + BuildTime //nolint[gochecknoglobals]
|
|
||||||
|
|
||||||
log = config.GetLogEntry("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() {
|
func main() {
|
||||||
if err := raven.SetDSN(DSNSentry); err != nil {
|
cmd.Main(
|
||||||
log.WithError(err).Errorln("Can not setup sentry DSN")
|
"ProtonMail Bridge",
|
||||||
}
|
"ProtonMail IMAP and SMTP Bridge",
|
||||||
raven.SetRelease(Revision)
|
[]cli.Flag{
|
||||||
bridge.UpdateCurrentUserAgent(Version, runtime.GOOS, "", "")
|
cli.BoolFlag{
|
||||||
|
Name: "no-window",
|
||||||
args.FilterProcessSerialNumberFromArgs()
|
Usage: "Don't show window after start"},
|
||||||
filterRestartNumberFromArgs()
|
cli.BoolFlag{
|
||||||
|
Name: "noninteractive",
|
||||||
app := cli.NewApp()
|
Usage: "Start Bridge entirely noninteractively"},
|
||||||
app.Name = "Protonmail Bridge"
|
},
|
||||||
app.Version = buildVersion
|
run,
|
||||||
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", Version).
|
|
||||||
WithField("revision", Revision).
|
|
||||||
WithField("runtime", runtime.GOOS).
|
|
||||||
WithField("build", BuildTime).
|
|
||||||
WithField("args", os.Args).
|
|
||||||
WithField("appLong", app.Name).
|
|
||||||
WithField("appShort", 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// run initializes and starts everything in a precise order.
|
// run initializes and starts everything in a precise order.
|
||||||
@ -174,13 +92,17 @@ func (ph *panicHandler) HandlePanic() {
|
|||||||
// IMPORTANT: ***Read the comments before CHANGING the order ***
|
// IMPORTANT: ***Read the comments before CHANGING the order ***
|
||||||
func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||||
// We need to have config instance to setup a logs, panic handler, etc ...
|
// We need to have config instance to setup a logs, panic handler, etc ...
|
||||||
cfg := config.New(AppShortName, Version, Revision, cacheVersion)
|
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
|
||||||
|
|
||||||
// We want to know about any problem. Our PanicHandler calls sentry which is
|
// 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
|
// 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
|
// 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.
|
// 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()
|
defer panicHandler.HandlePanic()
|
||||||
|
|
||||||
// First we need config and create necessary folder; it's dependency for everything.
|
// First we need config and create necessary folder; it's dependency for everything.
|
||||||
@ -192,13 +114,6 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
|||||||
logLevel := context.GlobalString("log-level")
|
logLevel := context.GlobalString("log-level")
|
||||||
debugClient, debugServer := config.SetupLog(cfg, logLevel)
|
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.
|
// 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.
|
// We should tell that to the user before we do anything else.
|
||||||
if context.Args().First() != "" {
|
if context.Args().First() != "" {
|
||||||
@ -208,12 +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.
|
// 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).
|
// (thus we put it before check of presence of other Bridge instance).
|
||||||
updates := updates.New(AppShortName, Version, Revision, BuildTime, bridge.ReleaseNotes, bridge.ReleaseFixedBugs, cfg.GetUpdateDir())
|
updates := updates.NewBridge(cfg.GetUpdateDir())
|
||||||
|
|
||||||
if dir := context.GlobalString("version-json"); dir != "" {
|
if dir := context.GlobalString("version-json"); dir != "" {
|
||||||
generateVersionFiles(updates, dir)
|
cmd.GenerateVersionFiles(updates, dir)
|
||||||
return nil
|
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.
|
// ClearOldData before starting new bridge to do a proper setup.
|
||||||
//
|
//
|
||||||
// IMPORTANT: If you the change position of this you will need to wait
|
// IMPORTANT: If you the change position of this you will need to wait
|
||||||
@ -240,7 +159,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Bridge is already running")
|
log.Warn("Bridge is already running")
|
||||||
if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil {
|
if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil {
|
||||||
numberOfCrashes = maxAllowedCrashes
|
cmd.DisableRestart()
|
||||||
log.Error("Second instance: ", err)
|
log.Error("Second instance: ", err)
|
||||||
}
|
}
|
||||||
return cli.NewExitError("Bridge is already running.", 3)
|
return cli.NewExitError("Bridge is already running.", 3)
|
||||||
@ -249,18 +168,12 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
|||||||
|
|
||||||
// In case user wants to do CPU or memory profiles...
|
// In case user wants to do CPU or memory profiles...
|
||||||
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
|
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
|
||||||
f, err := os.Create("cpu.pprof")
|
cmd.StartCPUProfile()
|
||||||
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)
|
|
||||||
}
|
|
||||||
defer pprof.StopCPUProfile()
|
defer pprof.StopCPUProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
|
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
|
||||||
defer makeMemoryProfile()
|
defer cmd.MakeMemoryProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we initialize all Bridge parts.
|
// Now we initialize all Bridge parts.
|
||||||
@ -268,14 +181,27 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
|||||||
eventListener := listener.New()
|
eventListener := listener.New()
|
||||||
events.SetupEvents(eventListener)
|
events.SetupEvents(eventListener)
|
||||||
|
|
||||||
credentialsStore, credentialsError := credentials.NewStore()
|
credentialsStore, credentialsError := credentials.NewStore(appName)
|
||||||
if credentialsError != nil {
|
if credentialsError != nil {
|
||||||
log.Error("Could not get credentials store: ", credentialsError)
|
log.Error("Could not get credentials store: ", credentialsError)
|
||||||
}
|
}
|
||||||
|
|
||||||
pmapiClientFactory := pmapifactory.New(cfg, eventListener)
|
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
|
||||||
|
|
||||||
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, Version, pmapiClientFactory, credentialsStore)
|
// 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))
|
||||||
|
|
||||||
|
// 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)
|
imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance)
|
||||||
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
|
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
|
||||||
|
|
||||||
@ -321,7 +247,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
|||||||
}
|
}
|
||||||
|
|
||||||
showWindowOnStart := !context.GlobalBool("no-window")
|
showWindowOnStart := !context.GlobalBool("no-window")
|
||||||
frontend := frontend.New(Version, buildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
|
frontend := frontend.New(constants.Version, constants.BuildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
|
||||||
|
|
||||||
// Last part is to start everything.
|
// Last part is to start everything.
|
||||||
log.Debug("Starting frontend...")
|
log.Debug("Starting frontend...")
|
||||||
@ -331,7 +257,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
|||||||
}
|
}
|
||||||
|
|
||||||
if frontend.IsAppRestarting() {
|
if frontend.IsAppRestarting() {
|
||||||
restartApp()
|
cmd.RestartApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -341,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.
|
// It will happen only when c10/prefs.json exists and c11/prefs.json not.
|
||||||
// No configuration changed between c10 and c11 versions.
|
// No configuration changed between c10 and c11 versions.
|
||||||
func migratePreferencesFromC10(cfg *config.Config) {
|
func migratePreferencesFromC10(cfg *config.Config) {
|
||||||
pref10Path := config.New(AppShortName, Version, Revision, "c10").GetPreferencesPath()
|
pref10Path := config.New(appName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
|
||||||
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
|
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
|
||||||
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
|
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
|
||||||
return
|
return
|
||||||
@ -359,7 +285,7 @@ func migratePreferencesFromC10(cfg *config.Config) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(pref11Path, data, 0644) //nolint[gosec]
|
err = ioutil.WriteFile(pref11Path, data, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("Problem to migrate preferences")
|
log.WithError(err).Error("Problem to migrate preferences")
|
||||||
return
|
return
|
||||||
@ -367,68 +293,3 @@ func migratePreferencesFromC10(cfg *config.Config) {
|
|||||||
|
|
||||||
log.Info("Preferences migrated")
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
177
cmd/Import-Export/main.go
Normal file
177
cmd/Import-Export/main.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// 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/cookies"
|
||||||
|
"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/preferences"
|
||||||
|
"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 (
|
||||||
|
// cacheVersion is used for cache files such as lock, or preferences.
|
||||||
|
// Different number will drop old files and create new ones.
|
||||||
|
cacheVersion = "c11"
|
||||||
|
|
||||||
|
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, 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 := &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))
|
||||||
|
|
||||||
|
pref := preferences.New(cfg)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
135
doc/importexport.md
Normal 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
|
||||||
|
```
|
||||||
@ -2,8 +2,13 @@
|
|||||||
|
|
||||||
Documentation pages in order to read for a novice:
|
Documentation pages in order to read for a novice:
|
||||||
|
|
||||||
* [Development cycle](development.md)
|
## Bridge
|
||||||
|
|
||||||
* [Bridge code](bridge.md)
|
* [Bridge code](bridge.md)
|
||||||
* [Internal Bridge database](database.md)
|
* [Internal Bridge database](database.md)
|
||||||
* [Communication between Bridge, Client and Server](communication.md)
|
* [Communication between Bridge, Client and Server](communication.md)
|
||||||
* [Encryption](encryption.md)
|
* [Encryption](encryption.md)
|
||||||
|
|
||||||
|
## Import-Export app
|
||||||
|
|
||||||
|
* [Import-Export code](importexport.md)
|
||||||
|
|||||||
77
go.mod
77
go.mod
@ -5,75 +5,78 @@ go 1.13
|
|||||||
// These dependencies are `replace`d below, so the version numbers should be ignored.
|
// These dependencies are `replace`d below, so the version numbers should be ignored.
|
||||||
// They are in a separate require block to highlight this.
|
// They are in a separate require block to highlight this.
|
||||||
require (
|
require (
|
||||||
github.com/docker/docker-credential-helpers v0.0.0-00010101000000-000000000000
|
github.com/docker/docker-credential-helpers v0.6.3
|
||||||
github.com/emersion/go-imap v0.0.0-20171113213225-939ec3994dbe
|
|
||||||
github.com/emersion/go-imap-quota v0.0.0-20171113212021-e883a2bc54d6
|
|
||||||
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
|
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
|
||||||
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
|
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
|
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.0
|
||||||
github.com/ProtonMail/go-appdir v1.1.0
|
github.com/ProtonMail/go-appdir v1.1.0
|
||||||
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
|
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
|
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
|
||||||
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee
|
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
|
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
|
||||||
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed
|
github.com/ProtonMail/gopenpgp/v2 v2.0.1
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
|
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
|
||||||
github.com/andybalholm/cascadia v1.1.0
|
|
||||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||||
github.com/chzyer/logex v1.1.10 // indirect
|
github.com/chzyer/logex v1.1.10 // indirect
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||||
github.com/cucumber/godog v0.8.1
|
github.com/cucumber/godog v0.8.1
|
||||||
github.com/danieljoos/wincred v1.0.2 // indirect
|
github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df
|
||||||
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42
|
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
|
||||||
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89
|
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
|
||||||
github.com/emersion/go-imap-move v0.0.0-20161227183138-88aef42b0f1d
|
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
|
||||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
|
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
|
||||||
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe
|
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075
|
||||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
|
||||||
|
github.com/emersion/go-mbox v1.0.0
|
||||||
|
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
|
||||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
|
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
|
||||||
github.com/fatih/color v1.9.0
|
github.com/fatih/color v1.9.0
|
||||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||||
github.com/getsentry/raven-go v0.2.0
|
github.com/getsentry/raven-go v0.2.0
|
||||||
github.com/go-resty/resty/v2 v2.2.0
|
github.com/go-resty/resty/v2 v2.3.0
|
||||||
github.com/golang/mock v1.4.3
|
github.com/golang/mock v1.4.4
|
||||||
github.com/google/go-cmp v0.4.0
|
github.com/google/go-cmp v0.5.1
|
||||||
|
github.com/google/uuid v1.1.1
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
|
||||||
github.com/hashicorp/go-multierror v1.0.0
|
github.com/hashicorp/go-multierror v1.1.0
|
||||||
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195
|
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
|
||||||
github.com/jhillyerd/enmime v0.8.0
|
|
||||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||||
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2
|
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
|
||||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
|
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||||
github.com/miekg/dns v1.1.29
|
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/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/pkg/errors v0.9.1
|
||||||
github.com/sirupsen/logrus v1.4.2
|
github.com/sirupsen/logrus v1.6.0
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
github.com/stretchr/testify v1.5.1
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41
|
github.com/stretchr/testify v1.6.1
|
||||||
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
|
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
|
||||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
|
|
||||||
github.com/twinj/uuid v1.0.0 // 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.3
|
go.etcd.io/bbolt v1.3.5
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
golang.org/x/text v0.3.3
|
||||||
golang.org/x/text v0.3.2
|
|
||||||
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
|
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.0.0
|
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||||
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f
|
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399
|
||||||
github.com/emersion/go-imap-quota => github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de
|
github.com/emersion/go-mbox => github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45
|
||||||
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
|
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
|
||||||
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
|
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
|
||||||
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f
|
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c
|
||||||
)
|
)
|
||||||
|
|||||||
207
go.sum
207
go.sum
@ -1,34 +1,37 @@
|
|||||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
|
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
|
||||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
|
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
|
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f h1:cFhATQTJGK2iZ0dc+jRhr75mh6bsc5Ug6NliaBya8Kw=
|
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
|
||||||
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.0.0 h1:0DQXbZNvUszWgXUuP7TzvQdwnkK1D5Zf/glBgCFJFCk=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.0.0/go.mod h1:R1gQindzdYFcWJuuGXteYHDJzUCVtyU+EpEqp9aWcFs=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||||
github.com/ProtonMail/go-appdir v1.0.0 h1:PZXQ0HkveuEugga3LeDycxWtybrXQfKR0ThxURd6ojw=
|
|
||||||
github.com/ProtonMail/go-appdir v1.0.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
|
|
||||||
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
|
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
|
||||||
github.com/ProtonMail/go-appdir v1.1.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
|
github.com/ProtonMail/go-appdir v1.1.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
|
||||||
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h1:YsSJ/mvZFYydQm/hRrt8R8UtgETixN2y3LK98f5LT60=
|
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h1:YsSJ/mvZFYydQm/hRrt8R8UtgETixN2y3LK98f5LT60=
|
||||||
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
|
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
|
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f h1:QkLm4yfhBQuBxrC46Vhy2sonOWVrwIJo5bgKpA82+TY=
|
github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399 h1:wBo/Xgb/Dn2loU47D+PJaOoIZ67i3AqYp51gLn8YE5U=
|
||||||
github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f/go.mod h1:+m2uLXghuYktgE/vc5AkmCxx1qhu33ZKHFWg1cGZPD0=
|
github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||||
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee h1:Q/nK7A9xzUimAZsQDa/yaw3xW9PkTTnJnkT5wAkXrmI=
|
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
|
||||||
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
||||||
github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de h1:+LA9teDYUwGkBvg0kqZPZetmxIv1r7s9/npBP1yzKs0=
|
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||||
github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de/go.mod h1:85zbnYVWIY7//iScX9fnB/kKOGH9B86YPqtpr7f1i7A=
|
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72 h1:hGCc4Oc2fD3I5mNnZ1VlREncVc9EXJF8dxW3sw16gWM=
|
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
|
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
|
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
|
||||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
|
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
|
||||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
|
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
|
||||||
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed h1:3gib6hGF61VfRu7cqqkODyRUgES5uF/fkLQanPPJiO8=
|
github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
|
||||||
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed/go.mod h1:NstNbZx1OIoyq+2qHAFLwDFpHbMk8L2i2Vr+LioJ3/g=
|
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
|
||||||
|
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45 h1:GDh55hDI2sNiirDqEWV8b6EB729u78Qxu3nKF970n6g=
|
||||||
|
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||||
@ -37,9 +40,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:m
|
|||||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
|
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 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
github.com/aslakhellesoy/gox v1.0.100/go.mod h1:AJl542QsKKG96COVsv0N74HHzVQgDIQPceVUh1aeU2M=
|
|
||||||
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=
|
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
|
||||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||||
@ -49,40 +49,33 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
|||||||
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 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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/cucumber/gherkin-go/v11 v11.0.0 h1:cwVwN1Qn2VRSfHZNLEh5x00tPBmZcjATBWDpxsR5Xug=
|
|
||||||
github.com/cucumber/gherkin-go/v11 v11.0.0/go.mod h1:CX33k2XU2qog4e+TFjOValoq6mIUq0DmVccZs238R9w=
|
|
||||||
github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8=
|
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/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
|
||||||
github.com/cucumber/godog v0.9.0 h1:QOb8wyC7f+FVFXzY3RdgowwJUb4WeJfqbnQqaH4jp+A=
|
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
|
||||||
github.com/cucumber/godog v0.9.0/go.mod h1:roWCHkpeK6UTOyIRRl7IR+fgfBeZ4vZR7OSq2J/NbM4=
|
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||||
github.com/cucumber/messages-go/v10 v10.0.1/go.mod h1:kA5T38CBlBbYLU12TIrJ4fk4wSkVVOgyh7Enyy8WnSg=
|
|
||||||
github.com/cucumber/messages-go/v10 v10.0.3 h1:m/9SD/K/A15WP7i1aemIv7cwvUw+viS51Ui5HBw1cdE=
|
|
||||||
github.com/cucumber/messages-go/v10 v10.0.3/go.mod h1:9jMZ2Y8ZxjLY6TG2+x344nt5rXstVVDYSdS5ySfI1WY=
|
|
||||||
github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
|
|
||||||
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42 h1:3TeZ5gy3We/LVL0sqmGhM8dFDTSM7Hyj7PMIdl6OTs4=
|
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
|
||||||
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||||
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89 h1:AzbVhcrxgJO5MfSvzG5q4IfrYVm0Jw4AHNPz47+DiR0=
|
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
|
||||||
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
|
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
|
||||||
github.com/emersion/go-imap-move v0.0.0-20161227173100-88aef42b0f1d h1:STRZFC+5HZITdsSFkhFfyYRb+tkiTwhxFz3sRW1lYjk=
|
|
||||||
github.com/emersion/go-imap-move v0.0.0-20161227173100-88aef42b0f1d/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
|
||||||
github.com/emersion/go-imap-move v0.0.0-20161227183138-88aef42b0f1d h1:E/ezdheD3QUe47cM0LpAPuJ6Pk1x0EFDmjoysaZhtaw=
|
|
||||||
github.com/emersion/go-imap-move v0.0.0-20161227183138-88aef42b0f1d/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
|
||||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
|
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
|
||||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
||||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
|
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU=
|
||||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
|
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
|
||||||
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe h1:2R2XpJkmbyy7PcSjnCPOnNfu+GuRzgWR9U2+j/d1O+0=
|
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 h1:z8TiDE4yqtzNeA1yb6ZRcktd+BHlXQbKGugvmDuc488=
|
||||||
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
|
||||||
github.com/emersion/go-imap-unselect v0.0.0-20161227193600-1e6dc73ac8fe h1:WeXweyFnbM2DQx0wxHkJKXYXwXpApopIeAjDTipW5Z4=
|
|
||||||
github.com/emersion/go-imap-unselect v0.0.0-20161227193600-1e6dc73ac8fe/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
|
||||||
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 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
|
||||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
||||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
|
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/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||||
|
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a h1:3C6qIGgPr1qAT0ikRD5NbyKpME/iHCDeXhpv/JJsFsE=
|
||||||
|
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1:kYIioST9GDHte9/BRWgi93rpqbDuFftMjKSMaXS8ABo=
|
||||||
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-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 h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
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=
|
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
|
||||||
@ -93,67 +86,60 @@ 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/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 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
|
||||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||||
github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
|
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
|
||||||
github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
|
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
|
||||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
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.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/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
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 h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
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 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
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.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||||
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE=
|
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195 h1:j0UEFmS7wSjAwKEIkgKBn8PRDfjcuggzr93R9wk53nQ=
|
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||||
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/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
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/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-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
|
||||||
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/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
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.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.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
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/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.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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
github.com/logrusorgru/aurora v2.0.3+incompatible/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.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
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-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
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 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
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.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
|
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
|
||||||
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
|
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
|
||||||
|
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6 h1:qsqscDgSJy+HqgMTR+3NwjYJBbp1+honwDsszLoS+pA=
|
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
|
||||||
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
|
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.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@ -161,13 +147,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
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.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 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||||
@ -179,31 +164,28 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
|
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
|
||||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
|
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
||||||
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
|
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/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
|
||||||
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
|
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
|
||||||
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
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-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
|
|
||||||
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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/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-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -211,34 +193,29 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/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-20191026070338-33540a1f6037/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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
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.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.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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
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/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/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 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
|
||||||
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
|
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
|
||||||
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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@ -32,10 +32,11 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log = config.GetLogEntry("api") //nolint[gochecknoglobals]
|
log = logrus.WithField("pkg", "api") //nolint[gochecknoglobals]
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiServer struct {
|
type apiServer struct {
|
||||||
|
|||||||
@ -15,55 +15,31 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Package bridge provides core business logic providing API over credentials store and PM API.
|
// Package bridge provides core functionality of Bridge app.
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||||
m "github.com/ProtonMail/proton-bridge/internal/metrics"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||||
logrus "github.com/sirupsen/logrus"
|
logrus "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
|
log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals]
|
||||||
isApplicationOutdated = false //nolint[gochecknoglobals]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bridge is a struct handling users.
|
|
||||||
type Bridge struct {
|
type Bridge struct {
|
||||||
config Configer
|
*users.Users
|
||||||
pref PreferenceProvider
|
|
||||||
panicHandler PanicHandler
|
|
||||||
events listener.Listener
|
|
||||||
version string
|
|
||||||
pmapiClientFactory PMAPIProviderFactory
|
|
||||||
credStorer CredentialsStorer
|
|
||||||
storeCache *store.Cache
|
|
||||||
|
|
||||||
// users is a list of accounts that have been added to bridge.
|
pref PreferenceProvider
|
||||||
// They are stored sorted in the credentials store in the order
|
clientManager users.ClientManager
|
||||||
// that they were added to bridge chronologically.
|
|
||||||
// People are used to that and so we preserve that ordering here.
|
|
||||||
users []*User
|
|
||||||
|
|
||||||
// idleUpdates is a channel which the imap backend listens to and which it uses
|
|
||||||
// to send idle updates to the mail client (eg thunderbird).
|
|
||||||
// The user stores should send idle updates on this channel.
|
|
||||||
idleUpdates chan interface{}
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
|
|
||||||
userAgentClientName string
|
userAgentClientName string
|
||||||
userAgentClientVersion string
|
userAgentClientVersion string
|
||||||
@ -73,46 +49,29 @@ type Bridge struct {
|
|||||||
func New(
|
func New(
|
||||||
config Configer,
|
config Configer,
|
||||||
pref PreferenceProvider,
|
pref PreferenceProvider,
|
||||||
panicHandler PanicHandler,
|
panicHandler users.PanicHandler,
|
||||||
eventListener listener.Listener,
|
eventListener listener.Listener,
|
||||||
version string,
|
clientManager users.ClientManager,
|
||||||
pmapiClientFactory PMAPIProviderFactory,
|
credStorer users.CredentialsStorer,
|
||||||
credStorer CredentialsStorer,
|
|
||||||
) *Bridge {
|
) *Bridge {
|
||||||
log.Trace("Creating new bridge")
|
// Allow DoH before starting the app if the user has previously set this setting.
|
||||||
|
|
||||||
b := &Bridge{
|
|
||||||
config: config,
|
|
||||||
pref: pref,
|
|
||||||
panicHandler: panicHandler,
|
|
||||||
events: eventListener,
|
|
||||||
version: version,
|
|
||||||
pmapiClientFactory: pmapiClientFactory,
|
|
||||||
credStorer: credStorer,
|
|
||||||
storeCache: store.NewCache(config.GetIMAPCachePath()),
|
|
||||||
idleUpdates: make(chan interface{}),
|
|
||||||
lock: sync.RWMutex{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow DoH before starting bridge if the user has previously set this setting.
|
|
||||||
// This allows us to start even if protonmail is blocked.
|
// This allows us to start even if protonmail is blocked.
|
||||||
if pref.GetBool(preferences.AllowProxyKey) {
|
if pref.GetBool(preferences.AllowProxyKey) {
|
||||||
AllowDoH()
|
clientManager.AllowProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
storeFactory := newStoreFactory(config, panicHandler, clientManager, eventListener)
|
||||||
defer panicHandler.HandlePanic()
|
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
|
||||||
b.watchBridgeOutdated()
|
b := &Bridge{
|
||||||
}()
|
Users: u,
|
||||||
|
|
||||||
if b.credStorer == nil {
|
pref: pref,
|
||||||
log.Error("Bridge has no credentials store")
|
clientManager: clientManager,
|
||||||
} else if err := b.loadUsersFromCredentialsStore(); err != nil {
|
|
||||||
log.WithError(err).Error("Could not load all users from credentials store")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if pref.GetBool(preferences.FirstStartKey) {
|
if pref.GetBool(preferences.FirstStartKey) {
|
||||||
b.SendMetric(m.New(m.Setup, m.FirstStart, m.Label(version)))
|
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
|
||||||
|
pref.SetBool(preferences.FirstStartKey, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
go b.heartbeat()
|
go b.heartbeat()
|
||||||
@ -122,325 +81,22 @@ func New(
|
|||||||
|
|
||||||
// heartbeat sends a heartbeat signal once a day.
|
// heartbeat sends a heartbeat signal once a day.
|
||||||
func (b *Bridge) heartbeat() {
|
func (b *Bridge) heartbeat() {
|
||||||
for range time.NewTicker(1 * time.Hour).C {
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
|
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
nextTime := time.Unix(next, 0)
|
nextTime := time.Unix(next, 0)
|
||||||
if time.Now().After(nextTime) {
|
if time.Now().After(nextTime) {
|
||||||
b.SendMetric(m.New(m.Heartbeat, m.Daily, m.NoLabel))
|
b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
|
||||||
nextTime = nextTime.Add(24 * time.Hour)
|
nextTime = nextTime.Add(24 * time.Hour)
|
||||||
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
|
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bridge) loadUsersFromCredentialsStore() (err error) {
|
|
||||||
b.lock.Lock()
|
|
||||||
defer b.lock.Unlock()
|
|
||||||
|
|
||||||
userIDs, err := b.credStorer.List()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, userID := range userIDs {
|
|
||||||
l := log.WithField("user", userID)
|
|
||||||
|
|
||||||
apiClient := b.pmapiClientFactory(userID)
|
|
||||||
|
|
||||||
user, newUserErr := newUser(b.panicHandler, userID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
|
|
||||||
if newUserErr != nil {
|
|
||||||
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
b.users = append(b.users, user)
|
|
||||||
|
|
||||||
if initUserErr := user.init(b.idleUpdates, apiClient); initUserErr != nil {
|
|
||||||
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) watchBridgeOutdated() {
|
|
||||||
ch := make(chan string)
|
|
||||||
b.events.Add(events.UpgradeApplicationEvent, ch)
|
|
||||||
for range ch {
|
|
||||||
isApplicationOutdated = true
|
|
||||||
b.closeAllConnections()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) closeAllConnections() {
|
|
||||||
for _, user := range b.users {
|
|
||||||
user.closeAllConnections()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login authenticates a user.
|
|
||||||
// The login flow:
|
|
||||||
// * Authenticate user:
|
|
||||||
// client, auth, err := bridge.Authenticate(username, password)
|
|
||||||
//
|
|
||||||
// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
|
|
||||||
// auth2FA, err := client.Auth2FA(twoFactorCode)
|
|
||||||
//
|
|
||||||
// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
|
|
||||||
// and then finish the login procedure.
|
|
||||||
// user, err := bridge.FinishLogin(client, auth, mailboxPassword)
|
|
||||||
func (b *Bridge) Login(username, password string) (loginClient PMAPIProvider, auth *pmapi.Auth, err error) {
|
|
||||||
log.WithField("username", username).Trace("Logging in to bridge")
|
|
||||||
|
|
||||||
b.crashBandicoot(username)
|
|
||||||
|
|
||||||
// We need to use "login" client because we need userID to properly
|
|
||||||
// assign access tokens into token manager.
|
|
||||||
loginClient = b.pmapiClientFactory("login")
|
|
||||||
|
|
||||||
authInfo, err := loginClient.AuthInfo(username)
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if auth, err = loginClient.Auth(username, password, authInfo); err != nil {
|
|
||||||
log.WithField("username", username).WithError(err).Error("Could not get auth for user")
|
|
||||||
return loginClient, auth, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return loginClient, auth, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FinishLogin finishes the login procedure and adds the user into the credentials store.
|
|
||||||
// See `Login` for more details of the login flow.
|
|
||||||
func (b *Bridge) FinishLogin(loginClient PMAPIProvider, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
|
|
||||||
log.Trace("Finishing bridge login")
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err == pmapi.ErrUpgradeApplication {
|
|
||||||
b.events.Emit(events.UpgradeApplicationEvent, "")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
b.lock.Lock()
|
|
||||||
defer b.lock.Unlock()
|
|
||||||
|
|
||||||
mbPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Could not hash mailbox password")
|
|
||||||
if logoutErr := loginClient.Logout(); logoutErr != nil {
|
|
||||||
log.WithError(logoutErr).Error("Clean login session after hash password failed.")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = loginClient.Unlock(mbPassword); err != nil {
|
|
||||||
log.WithError(err).Error("Could not decrypt keyring")
|
|
||||||
if logoutErr := loginClient.Logout(); logoutErr != nil {
|
|
||||||
log.WithError(logoutErr).Error("Clean login session after unlock failed.")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiUser, err := loginClient.CurrentUser()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Could not get login API user")
|
|
||||||
if logoutErr := loginClient.Logout(); logoutErr != nil {
|
|
||||||
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, hasUser := b.hasUser(apiUser.ID)
|
|
||||||
|
|
||||||
// If the user exists and is logged in, we don't want to do anything.
|
|
||||||
if hasUser && user.IsConnected() {
|
|
||||||
err = errors.New("user is already logged in")
|
|
||||||
log.WithError(err).Warn("User is already logged in")
|
|
||||||
if logoutErr := loginClient.Logout(); logoutErr != nil {
|
|
||||||
log.WithError(logoutErr).Warn("Could not discard auth generated during second login")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiToken := auth.UID() + ":" + auth.RefreshToken
|
|
||||||
apiClient := b.pmapiClientFactory(apiUser.ID)
|
|
||||||
auth, err = apiClient.AuthRefresh(apiToken)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Could refresh token in new client")
|
|
||||||
if logoutErr := loginClient.Logout(); logoutErr != nil {
|
|
||||||
log.WithError(logoutErr).Warn("Could not discard auth generated after auth refresh")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We load the current user again because it should now have addresses loaded.
|
|
||||||
apiUser, err = apiClient.CurrentUser()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Could not get current API user")
|
|
||||||
if logoutErr := loginClient.Logout(); logoutErr != nil {
|
|
||||||
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiToken = auth.UID() + ":" + auth.RefreshToken
|
|
||||||
activeEmails := apiClient.Addresses().ActiveEmails()
|
|
||||||
if _, err = b.credStorer.Add(apiUser.ID, apiUser.Name, apiToken, mbPassword, activeEmails); err != nil {
|
|
||||||
log.WithError(err).Error("Could not add user to credentials store")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a new user, generate the user object.
|
|
||||||
if !hasUser {
|
|
||||||
user, err = newUser(b.panicHandler, apiUser.ID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("user", apiUser.ID).WithError(err).Error("Could not create user")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the user auth and store (which we do for both new and existing users).
|
|
||||||
if err = user.init(b.idleUpdates, apiClient); err != nil {
|
|
||||||
log.WithField("user", user.userID).WithError(err).Error("Could not initialise user")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasUser {
|
|
||||||
b.users = append(b.users, user)
|
|
||||||
b.SendMetric(m.New(m.Setup, m.NewUser, m.NoLabel))
|
|
||||||
}
|
|
||||||
|
|
||||||
b.events.Emit(events.UserRefreshEvent, apiUser.ID)
|
|
||||||
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUsers returns all added users into keychain (even logged out users).
|
|
||||||
func (b *Bridge) GetUsers() []*User {
|
|
||||||
b.lock.RLock()
|
|
||||||
defer b.lock.RUnlock()
|
|
||||||
|
|
||||||
return b.users
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUser returns a user by `query` which is compared to users' ID, username
|
|
||||||
// or any attached e-mail address.
|
|
||||||
func (b *Bridge) GetUser(query string) (*User, error) {
|
|
||||||
b.crashBandicoot(query)
|
|
||||||
|
|
||||||
b.lock.RLock()
|
|
||||||
defer b.lock.RUnlock()
|
|
||||||
|
|
||||||
for _, user := range b.users {
|
|
||||||
if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
for _, address := range user.GetAddresses() {
|
|
||||||
if strings.EqualFold(address, query) {
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("user " + query + " not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearData closes all connections (to release db files and so on) and clears all data.
|
|
||||||
func (b *Bridge) ClearData() error {
|
|
||||||
var result *multierror.Error
|
|
||||||
for _, user := range b.users {
|
|
||||||
if err := user.Logout(); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
if err := user.closeStore(); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := b.config.ClearData(); err != nil {
|
|
||||||
result = multierror.Append(result, err)
|
|
||||||
}
|
|
||||||
return result.ErrorOrNil()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteUser deletes user completely; it logs user out from the API, stops any
|
|
||||||
// active connection, deletes from credentials store and removes from the Bridge struct.
|
|
||||||
func (b *Bridge) DeleteUser(userID string, clearStore bool) error {
|
|
||||||
b.lock.Lock()
|
|
||||||
defer b.lock.Unlock()
|
|
||||||
|
|
||||||
log := log.WithField("user", userID)
|
|
||||||
|
|
||||||
for idx, user := range b.users {
|
|
||||||
if user.ID() == userID {
|
|
||||||
if err := user.Logout(); err != nil {
|
|
||||||
log.WithError(err).Error("Cannot logout user")
|
|
||||||
// We can try to continue to remove the user.
|
|
||||||
// Token will still be valid, but will expire eventually.
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.closeStore(); err != nil {
|
|
||||||
log.WithError(err).Error("Failed to close user store")
|
|
||||||
}
|
|
||||||
if clearStore {
|
|
||||||
// Clear cache after closing connections (done in logout).
|
|
||||||
if err := user.clearStore(); err != nil {
|
|
||||||
log.WithError(err).Error("Failed to clear user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.credStorer.Delete(userID); err != nil {
|
|
||||||
log.WithError(err).Error("Cannot remove user")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.users = append(b.users[:idx], b.users[idx+1:]...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("user " + userID + " not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReportBug reports a new bug from the user.
|
|
||||||
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
|
|
||||||
apiClient := b.pmapiClientFactory("bug_reporter")
|
|
||||||
title := "[Bridge] Bug"
|
|
||||||
err := apiClient.ReportBugWithEmailClient(
|
|
||||||
osType,
|
|
||||||
osVersion,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
accountName,
|
|
||||||
address,
|
|
||||||
emailClient,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Reporting bug failed: ", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("Bug successfully reported")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMetric sends a metric. We don't want to return any errors, only log them.
|
|
||||||
func (b *Bridge) SendMetric(m m.Metric) {
|
|
||||||
apiClient := b.pmapiClientFactory("metric_reporter")
|
|
||||||
cat, act, lab := m.Get()
|
|
||||||
err := apiClient.SendSimpleMetric(string(cat), string(act), string(lab))
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Sending metric failed: ", err)
|
|
||||||
}
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"cat": cat,
|
|
||||||
"act": act,
|
|
||||||
"lab": lab,
|
|
||||||
}).Debug("Metric successfully sent")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
|
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
|
||||||
func (b *Bridge) GetCurrentClient() string {
|
func (b *Bridge) GetCurrentClient() string {
|
||||||
res := b.userAgentClientName
|
res := b.userAgentClientName
|
||||||
@ -455,56 +111,42 @@ func (b *Bridge) GetCurrentClient() string {
|
|||||||
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
|
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
|
||||||
b.userAgentClientName = clientName
|
b.userAgentClientName = clientName
|
||||||
b.userAgentClientVersion = clientVersion
|
b.userAgentClientVersion = clientVersion
|
||||||
b.updateCurrentUserAgent()
|
b.updateUserAgent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
|
// SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
|
||||||
// `runtime.GOOS`, but this can be overridden in case of better detection.
|
// `runtime.GOOS`, but this can be overridden in case of better detection.
|
||||||
func (b *Bridge) SetCurrentOS(os string) {
|
func (b *Bridge) SetCurrentOS(os string) {
|
||||||
b.userAgentOS = os
|
b.userAgentOS = os
|
||||||
b.updateCurrentUserAgent()
|
b.updateUserAgent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
|
func (b *Bridge) updateUserAgent() {
|
||||||
func (b *Bridge) GetIMAPUpdatesChannel() chan interface{} {
|
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
|
||||||
if b.idleUpdates == nil {
|
}
|
||||||
log.Warn("Bridge updates channel is nil")
|
|
||||||
|
// ReportBug reports a new bug from the user.
|
||||||
|
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
|
||||||
|
c := b.clientManager.GetAnonymousClient()
|
||||||
|
defer c.Logout()
|
||||||
|
|
||||||
|
title := "[Bridge] Bug"
|
||||||
|
report := pmapi.ReportReq{
|
||||||
|
OS: osType,
|
||||||
|
OSVersion: osVersion,
|
||||||
|
Browser: emailClient,
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Username: accountName,
|
||||||
|
Email: address,
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.idleUpdates
|
if err := c.Report(report); err != nil {
|
||||||
}
|
log.Error("Reporting bug failed: ", err)
|
||||||
|
return err
|
||||||
// AllowDoH instructs bridge to use DoH to access an API proxy if necessary.
|
|
||||||
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
|
|
||||||
func AllowDoH() {
|
|
||||||
pmapi.GlobalAllowDoH()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisallowDoH instructs bridge to not use DoH to access an API proxy if necessary.
|
|
||||||
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
|
|
||||||
func DisallowDoH() {
|
|
||||||
pmapi.GlobalDisallowDoH()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) updateCurrentUserAgent() {
|
|
||||||
UpdateCurrentUserAgent(b.version, b.userAgentOS, b.userAgentClientName, b.userAgentClientVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasUser returns whether the bridge currently has a user with ID `id`.
|
|
||||||
func (b *Bridge) hasUser(id string) (user *User, ok bool) {
|
|
||||||
for _, u := range b.users {
|
|
||||||
if u.ID() == id {
|
|
||||||
user, ok = u, true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
log.Info("Bug successfully reported")
|
||||||
}
|
|
||||||
|
|
||||||
// "Easter egg" for testing purposes.
|
return nil
|
||||||
func (b *Bridge) crashBandicoot(username string) {
|
|
||||||
if username == "crash@bandicoot" {
|
|
||||||
panic("Your wish is my command… I crash!")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,233 +0,0 @@
|
|||||||
// 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 bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBridgeFinishLoginBadPassword(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
// Init bridge with no user from keychain.
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
|
||||||
|
|
||||||
// Set up mocks for FinishLogin.
|
|
||||||
err := errors.New("bad password")
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, err)
|
|
||||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
|
||||||
|
|
||||||
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridgeFinishLoginUpgradeApplication(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
// Init bridge with no user from keychain.
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
|
||||||
|
|
||||||
// Set up mocks for FinishLogin.
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, pmapi.ErrUpgradeApplication)
|
|
||||||
|
|
||||||
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
|
|
||||||
err := errors.New("Cannot logout when upgrade needed")
|
|
||||||
m.pmapiClient.EXPECT().Logout().Return(err)
|
|
||||||
|
|
||||||
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshWithToken(token string) *pmapi.Auth {
|
|
||||||
return &pmapi.Auth{
|
|
||||||
RefreshToken: token,
|
|
||||||
KeySalt: "", // No salting in tests.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func credentialsWithToken(token string) *credentials.Credentials {
|
|
||||||
tmp := &credentials.Credentials{}
|
|
||||||
*tmp = *testCredentials
|
|
||||||
tmp.APIToken = token
|
|
||||||
return tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridgeFinishLoginNewUser(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
// Bridge finds no users in the keychain.
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
|
||||||
|
|
||||||
// Get user to be able to setup new client with proper userID.
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
|
||||||
|
|
||||||
// Setup of new client.
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
|
|
||||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
|
|
||||||
// Set up mocks for authorising the new user (in user.init).
|
|
||||||
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil).Times(2)
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
|
|
||||||
|
|
||||||
// Set up mocks for creating the user's store (in store.New).
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
|
||||||
|
|
||||||
// Emit event for new user and send metrics.
|
|
||||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
|
||||||
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.NewUser), string(metrics.NoLabel))
|
|
||||||
|
|
||||||
// Set up mocks for starting the store's event loop (in store.New).
|
|
||||||
// The event loop runs in another goroutine so this might happen at any time.
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
|
||||||
|
|
||||||
// Set up mocks for performing the initial store sync.
|
|
||||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
|
||||||
|
|
||||||
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridgeFinishLoginExistingUser(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
loggedOutCreds := *testCredentials
|
|
||||||
loggedOutCreds.APIToken = ""
|
|
||||||
loggedOutCreds.MailboxPassword = ""
|
|
||||||
|
|
||||||
// Bridge finds one logged out user in the keychain.
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
|
||||||
// New user
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
|
|
||||||
// Init user
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrInvalidToken)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
|
||||||
|
|
||||||
// Get user to be able to setup new client with proper userID.
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
|
||||||
|
|
||||||
// Setup of new client.
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
|
|
||||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
|
|
||||||
// Set up mocks for authorising the new user (in user.init).
|
|
||||||
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil)
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
|
|
||||||
|
|
||||||
// Set up mocks for creating the user's store (in store.New).
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
|
||||||
|
|
||||||
// Reload account list in GUI.
|
|
||||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
|
||||||
|
|
||||||
// Set up mocks for starting the store's event loop (in store.New)
|
|
||||||
// The event loop runs in another goroutine so this might happen at any time.
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
|
||||||
|
|
||||||
// Set up mocks for performing the initial store sync.
|
|
||||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
|
||||||
|
|
||||||
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridgeDoubleLogin(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
// Firstly, start bridge with existing user...
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
|
||||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
|
||||||
|
|
||||||
bridge := testNewBridge(t, m)
|
|
||||||
defer cleanUpBridgeUserData(bridge)
|
|
||||||
|
|
||||||
// Then, try to log in again...
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
|
||||||
m.pmapiClient.EXPECT().Logout()
|
|
||||||
|
|
||||||
_, err := bridge.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
|
|
||||||
assert.Equal(t, "user is already logged in", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) {
|
|
||||||
bridge := testNewBridge(t, m)
|
|
||||||
defer cleanUpBridgeUserData(bridge)
|
|
||||||
|
|
||||||
user, err := bridge.FinishLogin(m.pmapiClient, auth, mailboxPassword)
|
|
||||||
|
|
||||||
waitForEvents()
|
|
||||||
|
|
||||||
assert.Equal(t, expectedErr, err)
|
|
||||||
|
|
||||||
if expectedUserID != "" {
|
|
||||||
assert.Equal(t, expectedUserID, user.ID())
|
|
||||||
assert.Equal(t, 1, len(bridge.users))
|
|
||||||
assert.Equal(t, expectedUserID, bridge.users[0].ID())
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, (*User)(nil), user)
|
|
||||||
assert.Equal(t, 0, len(bridge.users))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
// 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 bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewBridgeNoKeychain(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain"))
|
|
||||||
|
|
||||||
checkBridgeNew(t, m, []*credentials.Credentials{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBridgeWithoutUsersInCredentialsStore(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
|
||||||
|
|
||||||
checkBridgeNew(t, m, []*credentials.Credentials{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBridgeWithDisconnectedUser(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
|
||||||
|
|
||||||
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token"))
|
|
||||||
|
|
||||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
|
||||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
|
||||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(nil)
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
|
||||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
|
||||||
|
|
||||||
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBridgeWithConnectedUser(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
|
||||||
|
|
||||||
// Set up mocks for store initialisation for the authorized user.
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
|
||||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
|
||||||
|
|
||||||
checkBridgeNew(t, m, []*credentials.Credentials{testCredentials})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests two users with different states and checks also the order from
|
|
||||||
// credentials store is kept also in array of Bridge users.
|
|
||||||
func TestNewBridgeWithUsers(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{"user", "user"}, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
|
||||||
|
|
||||||
// Set up mocks for store initialisation for the unauth user.
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
|
||||||
|
|
||||||
// Set up mocks for store initialisation for the authorized user.
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
|
||||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
|
||||||
|
|
||||||
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBridgeFirstStart(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(true)
|
|
||||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.FirstStart), gomock.Any())
|
|
||||||
|
|
||||||
testNewBridge(t, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkBridgeNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
|
|
||||||
bridge := testNewBridge(t, m)
|
|
||||||
defer cleanUpBridgeUserData(bridge)
|
|
||||||
|
|
||||||
assert.Equal(m.t, len(expectedCredentials), len(bridge.GetUsers()))
|
|
||||||
|
|
||||||
credentials := []*credentials.Credentials{}
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
credentials = append(credentials, user.creds)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(m.t, expectedCredentials, credentials)
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
// 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 bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetNoUser(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
checkBridgeGetUser(t, m, "nouser", -1, "user nouser not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetUserByID(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
checkBridgeGetUser(t, m, "user", 0, "")
|
|
||||||
checkBridgeGetUser(t, m, "users", 1, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetUserByName(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
checkBridgeGetUser(t, m, "username", 0, "")
|
|
||||||
checkBridgeGetUser(t, m, "usersname", 1, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetUserByEmail(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
checkBridgeGetUser(t, m, "user@pm.me", 0, "")
|
|
||||||
checkBridgeGetUser(t, m, "users@pm.me", 1, "")
|
|
||||||
checkBridgeGetUser(t, m, "anotheruser@pm.me", 1, "")
|
|
||||||
checkBridgeGetUser(t, m, "alsouser@pm.me", 1, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteUser(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
bridge := testNewBridgeWithUsers(t, m)
|
|
||||||
defer cleanUpBridgeUserData(bridge)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
m.credentialsStore.EXPECT().Delete("user").Return(nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
|
|
||||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
|
||||||
|
|
||||||
err := bridge.DeleteUser("user", true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 1, len(bridge.users))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Even when logout fails, delete is done.
|
|
||||||
func TestDeleteUserWithFailingLogout(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
bridge := testNewBridgeWithUsers(t, m)
|
|
||||||
defer cleanUpBridgeUserData(bridge)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed"))
|
|
||||||
m.credentialsStore.EXPECT().Delete("user").Return(nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
|
|
||||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
|
||||||
|
|
||||||
err := bridge.DeleteUser("user", true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 1, len(bridge.users))
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
|
|
||||||
bridge := testNewBridgeWithUsers(t, m)
|
|
||||||
defer cleanUpBridgeUserData(bridge)
|
|
||||||
|
|
||||||
user, err := bridge.GetUser(query)
|
|
||||||
waitForEvents()
|
|
||||||
|
|
||||||
if expectedError != "" {
|
|
||||||
assert.Equal(m.t, expectedError, err.Error())
|
|
||||||
} else {
|
|
||||||
assert.NoError(m.t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedUser *User
|
|
||||||
if index >= 0 {
|
|
||||||
expectedUser = bridge.users[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(m.t, expectedUser, user)
|
|
||||||
}
|
|
||||||
@ -15,8 +15,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Code generated by ./credits.sh at Thu Apr 16 13:43:04 CEST 2020. DO NOT EDIT.
|
// Code generated by ./credits.sh at Tue Sep 29 14:56:25 CEST 2020. DO NOT EDIT.
|
||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-imap-quota;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp;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/danieljoos/wincred;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/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;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;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;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/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;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;"
|
||||||
|
|||||||
@ -1,924 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: github.com/ProtonMail/proton-bridge/internal/bridge (interfaces: Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer)
|
|
||||||
|
|
||||||
// Package mocks is a generated GoMock package.
|
|
||||||
package mocks
|
|
||||||
|
|
||||||
import (
|
|
||||||
io "io"
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
crypto "github.com/ProtonMail/gopenpgp/crypto"
|
|
||||||
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
|
|
||||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockConfiger is a mock of Configer interface
|
|
||||||
type MockConfiger struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockConfigerMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockConfigerMockRecorder is the mock recorder for MockConfiger
|
|
||||||
type MockConfigerMockRecorder struct {
|
|
||||||
mock *MockConfiger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockConfiger creates a new mock instance
|
|
||||||
func NewMockConfiger(ctrl *gomock.Controller) *MockConfiger {
|
|
||||||
mock := &MockConfiger{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockConfigerMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
|
||||||
func (m *MockConfiger) EXPECT() *MockConfigerMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearData mocks base method
|
|
||||||
func (m *MockConfiger) ClearData() error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ClearData")
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearData indicates an expected call of ClearData
|
|
||||||
func (mr *MockConfigerMockRecorder) ClearData() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearData", reflect.TypeOf((*MockConfiger)(nil).ClearData))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAPIConfig mocks base method
|
|
||||||
func (m *MockConfiger) GetAPIConfig() *pmapi.ClientConfig {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAPIConfig")
|
|
||||||
ret0, _ := ret[0].(*pmapi.ClientConfig)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAPIConfig indicates an expected call of GetAPIConfig
|
|
||||||
func (mr *MockConfigerMockRecorder) GetAPIConfig() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIConfig", reflect.TypeOf((*MockConfiger)(nil).GetAPIConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDBDir mocks base method
|
|
||||||
func (m *MockConfiger) GetDBDir() string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetDBDir")
|
|
||||||
ret0, _ := ret[0].(string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDBDir indicates an expected call of GetDBDir
|
|
||||||
func (mr *MockConfigerMockRecorder) GetDBDir() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBDir", reflect.TypeOf((*MockConfiger)(nil).GetDBDir))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIMAPCachePath mocks base method
|
|
||||||
func (m *MockConfiger) GetIMAPCachePath() string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetIMAPCachePath")
|
|
||||||
ret0, _ := ret[0].(string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIMAPCachePath indicates an expected call of GetIMAPCachePath
|
|
||||||
func (mr *MockConfigerMockRecorder) GetIMAPCachePath() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIMAPCachePath", reflect.TypeOf((*MockConfiger)(nil).GetIMAPCachePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockPreferenceProvider is a mock of PreferenceProvider interface
|
|
||||||
type MockPreferenceProvider struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockPreferenceProviderMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockPreferenceProviderMockRecorder is the mock recorder for MockPreferenceProvider
|
|
||||||
type MockPreferenceProviderMockRecorder struct {
|
|
||||||
mock *MockPreferenceProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockPreferenceProvider creates a new mock instance
|
|
||||||
func NewMockPreferenceProvider(ctrl *gomock.Controller) *MockPreferenceProvider {
|
|
||||||
mock := &MockPreferenceProvider{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockPreferenceProviderMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
|
||||||
func (m *MockPreferenceProvider) EXPECT() *MockPreferenceProviderMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get mocks base method
|
|
||||||
func (m *MockPreferenceProvider) Get(arg0 string) string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Get", arg0)
|
|
||||||
ret0, _ := ret[0].(string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get indicates an expected call of Get
|
|
||||||
func (mr *MockPreferenceProviderMockRecorder) Get(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPreferenceProvider)(nil).Get), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBool mocks base method
|
|
||||||
func (m *MockPreferenceProvider) GetBool(arg0 string) bool {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetBool", arg0)
|
|
||||||
ret0, _ := ret[0].(bool)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBool indicates an expected call of GetBool
|
|
||||||
func (mr *MockPreferenceProviderMockRecorder) GetBool(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockPreferenceProvider)(nil).GetBool), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInt mocks base method
|
|
||||||
func (m *MockPreferenceProvider) GetInt(arg0 string) int {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetInt", arg0)
|
|
||||||
ret0, _ := ret[0].(int)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInt indicates an expected call of GetInt
|
|
||||||
func (mr *MockPreferenceProviderMockRecorder) GetInt(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockPreferenceProvider)(nil).GetInt), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set mocks base method
|
|
||||||
func (m *MockPreferenceProvider) Set(arg0, arg1 string) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
m.ctrl.Call(m, "Set", arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set indicates an expected call of Set
|
|
||||||
func (mr *MockPreferenceProviderMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockPreferenceProvider)(nil).Set), arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockPanicHandler is a mock of PanicHandler interface
|
|
||||||
type MockPanicHandler struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockPanicHandlerMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler
|
|
||||||
type MockPanicHandlerMockRecorder struct {
|
|
||||||
mock *MockPanicHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockPanicHandler creates a new mock instance
|
|
||||||
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
|
|
||||||
mock := &MockPanicHandler{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockPanicHandlerMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
|
||||||
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandlePanic mocks base method
|
|
||||||
func (m *MockPanicHandler) HandlePanic() {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
m.ctrl.Call(m, "HandlePanic")
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandlePanic indicates an expected call of HandlePanic
|
|
||||||
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockPMAPIProvider is a mock of PMAPIProvider interface
|
|
||||||
type MockPMAPIProvider struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockPMAPIProviderMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockPMAPIProviderMockRecorder is the mock recorder for MockPMAPIProvider
|
|
||||||
type MockPMAPIProviderMockRecorder struct {
|
|
||||||
mock *MockPMAPIProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockPMAPIProvider creates a new mock instance
|
|
||||||
func NewMockPMAPIProvider(ctrl *gomock.Controller) *MockPMAPIProvider {
|
|
||||||
mock := &MockPMAPIProvider{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockPMAPIProviderMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
|
||||||
func (m *MockPMAPIProvider) EXPECT() *MockPMAPIProviderMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addresses mocks base method
|
|
||||||
func (m *MockPMAPIProvider) Addresses() pmapi.AddressList {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Addresses")
|
|
||||||
ret0, _ := ret[0].(pmapi.AddressList)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addresses indicates an expected call of Addresses
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) Addresses() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addresses", reflect.TypeOf((*MockPMAPIProvider)(nil).Addresses))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth mocks base method
|
|
||||||
func (m *MockPMAPIProvider) Auth(arg0, arg1 string, arg2 *pmapi.AuthInfo) (*pmapi.Auth, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Auth", arg0, arg1, arg2)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Auth)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth indicates an expected call of Auth
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) Auth(arg0, arg1, arg2 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth", reflect.TypeOf((*MockPMAPIProvider)(nil).Auth), arg0, arg1, arg2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth2FA mocks base method
|
|
||||||
func (m *MockPMAPIProvider) Auth2FA(arg0 string, arg1 *pmapi.Auth) (*pmapi.Auth2FA, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Auth2FA", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Auth2FA)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth2FA indicates an expected call of Auth2FA
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) Auth2FA(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth2FA", reflect.TypeOf((*MockPMAPIProvider)(nil).Auth2FA), arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthInfo mocks base method
|
|
||||||
func (m *MockPMAPIProvider) AuthInfo(arg0 string) (*pmapi.AuthInfo, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "AuthInfo", arg0)
|
|
||||||
ret0, _ := ret[0].(*pmapi.AuthInfo)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthInfo indicates an expected call of AuthInfo
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) AuthInfo(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthInfo", reflect.TypeOf((*MockPMAPIProvider)(nil).AuthInfo), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthRefresh mocks base method
|
|
||||||
func (m *MockPMAPIProvider) AuthRefresh(arg0 string) (*pmapi.Auth, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "AuthRefresh", arg0)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Auth)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthRefresh indicates an expected call of AuthRefresh
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) AuthRefresh(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthRefresh", reflect.TypeOf((*MockPMAPIProvider)(nil).AuthRefresh), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountMessages mocks base method
|
|
||||||
func (m *MockPMAPIProvider) CountMessages(arg0 string) ([]*pmapi.MessagesCount, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CountMessages", arg0)
|
|
||||||
ret0, _ := ret[0].([]*pmapi.MessagesCount)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountMessages indicates an expected call of CountMessages
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) CountMessages(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).CountMessages), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAttachment mocks base method
|
|
||||||
func (m *MockPMAPIProvider) CreateAttachment(arg0 *pmapi.Attachment, arg1, arg2 io.Reader) (*pmapi.Attachment, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CreateAttachment", arg0, arg1, arg2)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Attachment)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAttachment indicates an expected call of CreateAttachment
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) CreateAttachment(arg0, arg1, arg2 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAttachment", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateAttachment), arg0, arg1, arg2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDraft mocks base method
|
|
||||||
func (m *MockPMAPIProvider) CreateDraft(arg0 *pmapi.Message, arg1 string, arg2 int) (*pmapi.Message, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CreateDraft", arg0, arg1, arg2)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Message)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDraft indicates an expected call of CreateDraft
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) CreateDraft(arg0, arg1, arg2 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDraft", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateDraft), arg0, arg1, arg2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateLabel mocks base method
|
|
||||||
func (m *MockPMAPIProvider) CreateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CreateLabel", arg0)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Label)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateLabel indicates an expected call of CreateLabel
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) CreateLabel(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateLabel), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentUser mocks base method
|
|
||||||
func (m *MockPMAPIProvider) CurrentUser() (*pmapi.User, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CurrentUser")
|
|
||||||
ret0, _ := ret[0].(*pmapi.User)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentUser indicates an expected call of CurrentUser
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) CurrentUser() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentUser", reflect.TypeOf((*MockPMAPIProvider)(nil).CurrentUser))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecryptAndVerifyCards mocks base method
|
|
||||||
func (m *MockPMAPIProvider) DecryptAndVerifyCards(arg0 []pmapi.Card) ([]pmapi.Card, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DecryptAndVerifyCards", arg0)
|
|
||||||
ret0, _ := ret[0].([]pmapi.Card)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecryptAndVerifyCards indicates an expected call of DecryptAndVerifyCards
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) DecryptAndVerifyCards(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptAndVerifyCards", reflect.TypeOf((*MockPMAPIProvider)(nil).DecryptAndVerifyCards), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteLabel mocks base method
|
|
||||||
func (m *MockPMAPIProvider) DeleteLabel(arg0 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteLabel", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteLabel indicates an expected call of DeleteLabel
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) DeleteLabel(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).DeleteLabel), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMessages mocks base method
|
|
||||||
func (m *MockPMAPIProvider) DeleteMessages(arg0 []string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteMessages", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMessages indicates an expected call of DeleteMessages
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) DeleteMessages(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).DeleteMessages), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmptyFolder mocks base method
|
|
||||||
func (m *MockPMAPIProvider) EmptyFolder(arg0, arg1 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "EmptyFolder", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmptyFolder indicates an expected call of EmptyFolder
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) EmptyFolder(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EmptyFolder", reflect.TypeOf((*MockPMAPIProvider)(nil).EmptyFolder), arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttachment mocks base method
|
|
||||||
func (m *MockPMAPIProvider) GetAttachment(arg0 string) (io.ReadCloser, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAttachment", arg0)
|
|
||||||
ret0, _ := ret[0].(io.ReadCloser)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttachment indicates an expected call of GetAttachment
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) GetAttachment(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockPMAPIProvider)(nil).GetAttachment), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContactByID mocks base method
|
|
||||||
func (m *MockPMAPIProvider) GetContactByID(arg0 string) (pmapi.Contact, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetContactByID", arg0)
|
|
||||||
ret0, _ := ret[0].(pmapi.Contact)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContactByID indicates an expected call of GetContactByID
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) GetContactByID(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactByID", reflect.TypeOf((*MockPMAPIProvider)(nil).GetContactByID), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContactEmailByEmail mocks base method
|
|
||||||
func (m *MockPMAPIProvider) GetContactEmailByEmail(arg0 string, arg1, arg2 int) ([]pmapi.ContactEmail, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetContactEmailByEmail", arg0, arg1, arg2)
|
|
||||||
ret0, _ := ret[0].([]pmapi.ContactEmail)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContactEmailByEmail indicates an expected call of GetContactEmailByEmail
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) GetContactEmailByEmail(arg0, arg1, arg2 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactEmailByEmail", reflect.TypeOf((*MockPMAPIProvider)(nil).GetContactEmailByEmail), arg0, arg1, arg2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEvent mocks base method
|
|
||||||
func (m *MockPMAPIProvider) GetEvent(arg0 string) (*pmapi.Event, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetEvent", arg0)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Event)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEvent indicates an expected call of GetEvent
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) GetEvent(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvent", reflect.TypeOf((*MockPMAPIProvider)(nil).GetEvent), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMailSettings mocks base method
|
|
||||||
func (m *MockPMAPIProvider) GetMailSettings() (pmapi.MailSettings, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetMailSettings")
|
|
||||||
ret0, _ := ret[0].(pmapi.MailSettings)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMailSettings indicates an expected call of GetMailSettings
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) GetMailSettings() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailSettings", reflect.TypeOf((*MockPMAPIProvider)(nil).GetMailSettings))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessage mocks base method
|
|
||||||
func (m *MockPMAPIProvider) GetMessage(arg0 string) (*pmapi.Message, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetMessage", arg0)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Message)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessage indicates an expected call of GetMessage
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) GetMessage(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockPMAPIProvider)(nil).GetMessage), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPublicKeysForEmail mocks base method
|
|
||||||
func (m *MockPMAPIProvider) GetPublicKeysForEmail(arg0 string) ([]pmapi.PublicKey, bool, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetPublicKeysForEmail", arg0)
|
|
||||||
ret0, _ := ret[0].([]pmapi.PublicKey)
|
|
||||||
ret1, _ := ret[1].(bool)
|
|
||||||
ret2, _ := ret[2].(error)
|
|
||||||
return ret0, ret1, ret2
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPublicKeysForEmail indicates an expected call of GetPublicKeysForEmail
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) GetPublicKeysForEmail(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeysForEmail", reflect.TypeOf((*MockPMAPIProvider)(nil).GetPublicKeysForEmail), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import mocks base method
|
|
||||||
func (m *MockPMAPIProvider) Import(arg0 []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Import", arg0)
|
|
||||||
ret0, _ := ret[0].([]*pmapi.ImportMsgRes)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import indicates an expected call of Import
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) Import(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*MockPMAPIProvider)(nil).Import), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeyRingForAddressID mocks base method
|
|
||||||
func (m *MockPMAPIProvider) KeyRingForAddressID(arg0 string) *crypto.KeyRing {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
|
|
||||||
ret0, _ := ret[0].(*crypto.KeyRing)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockPMAPIProvider)(nil).KeyRingForAddressID), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabelMessages mocks base method
|
|
||||||
func (m *MockPMAPIProvider) LabelMessages(arg0 []string, arg1 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "LabelMessages", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabelMessages indicates an expected call of LabelMessages
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) LabelMessages(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).LabelMessages), arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLabels mocks base method
|
|
||||||
func (m *MockPMAPIProvider) ListLabels() ([]*pmapi.Label, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListLabels")
|
|
||||||
ret0, _ := ret[0].([]*pmapi.Label)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLabels indicates an expected call of ListLabels
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) ListLabels() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLabels", reflect.TypeOf((*MockPMAPIProvider)(nil).ListLabels))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListMessages mocks base method
|
|
||||||
func (m *MockPMAPIProvider) ListMessages(arg0 *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListMessages", arg0)
|
|
||||||
ret0, _ := ret[0].([]*pmapi.Message)
|
|
||||||
ret1, _ := ret[1].(int)
|
|
||||||
ret2, _ := ret[2].(error)
|
|
||||||
return ret0, ret1, ret2
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListMessages indicates an expected call of ListMessages
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) ListMessages(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).ListMessages), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout mocks base method
|
|
||||||
func (m *MockPMAPIProvider) Logout() error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Logout")
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout indicates an expected call of Logout
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) Logout() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockPMAPIProvider)(nil).Logout))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkMessagesRead mocks base method
|
|
||||||
func (m *MockPMAPIProvider) MarkMessagesRead(arg0 []string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "MarkMessagesRead", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkMessagesRead indicates an expected call of MarkMessagesRead
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) MarkMessagesRead(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesRead", reflect.TypeOf((*MockPMAPIProvider)(nil).MarkMessagesRead), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkMessagesUnread mocks base method
|
|
||||||
func (m *MockPMAPIProvider) MarkMessagesUnread(arg0 []string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "MarkMessagesUnread", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkMessagesUnread indicates an expected call of MarkMessagesUnread
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) MarkMessagesUnread(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesUnread", reflect.TypeOf((*MockPMAPIProvider)(nil).MarkMessagesUnread), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReportBugWithEmailClient mocks base method
|
|
||||||
func (m *MockPMAPIProvider) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ReportBugWithEmailClient", arg0, arg1, arg2, arg3, arg4, arg5, arg6)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReportBugWithEmailClient indicates an expected call of ReportBugWithEmailClient
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBugWithEmailClient", reflect.TypeOf((*MockPMAPIProvider)(nil).ReportBugWithEmailClient), arg0, arg1, arg2, arg3, arg4, arg5, arg6)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessage mocks base method
|
|
||||||
func (m *MockPMAPIProvider) SendMessage(arg0 string, arg1 *pmapi.SendMessageReq) (*pmapi.Message, *pmapi.Message, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SendMessage", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Message)
|
|
||||||
ret1, _ := ret[1].(*pmapi.Message)
|
|
||||||
ret2, _ := ret[2].(error)
|
|
||||||
return ret0, ret1, ret2
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessage indicates an expected call of SendMessage
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) SendMessage(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockPMAPIProvider)(nil).SendMessage), arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendSimpleMetric mocks base method
|
|
||||||
func (m *MockPMAPIProvider) SendSimpleMetric(arg0, arg1, arg2 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SendSimpleMetric", arg0, arg1, arg2)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendSimpleMetric indicates an expected call of SendSimpleMetric
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) SendSimpleMetric(arg0, arg1, arg2 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendSimpleMetric", reflect.TypeOf((*MockPMAPIProvider)(nil).SendSimpleMetric), arg0, arg1, arg2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAuths mocks base method
|
|
||||||
func (m *MockPMAPIProvider) SetAuths(arg0 chan<- *pmapi.Auth) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
m.ctrl.Call(m, "SetAuths", arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAuths indicates an expected call of SetAuths
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) SetAuths(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAuths", reflect.TypeOf((*MockPMAPIProvider)(nil).SetAuths), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnlabelMessages mocks base method
|
|
||||||
func (m *MockPMAPIProvider) UnlabelMessages(arg0 []string, arg1 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UnlabelMessages", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnlabelMessages indicates an expected call of UnlabelMessages
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) UnlabelMessages(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlabelMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).UnlabelMessages), arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock mocks base method
|
|
||||||
func (m *MockPMAPIProvider) Unlock(arg0 string) (*crypto.KeyRing, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Unlock", arg0)
|
|
||||||
ret0, _ := ret[0].(*crypto.KeyRing)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock indicates an expected call of Unlock
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) Unlock(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockPMAPIProvider)(nil).Unlock), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnlockAddresses mocks base method
|
|
||||||
func (m *MockPMAPIProvider) UnlockAddresses(arg0 []byte) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UnlockAddresses", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnlockAddresses indicates an expected call of UnlockAddresses
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) UnlockAddresses(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlockAddresses", reflect.TypeOf((*MockPMAPIProvider)(nil).UnlockAddresses), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLabel mocks base method
|
|
||||||
func (m *MockPMAPIProvider) UpdateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdateLabel", arg0)
|
|
||||||
ret0, _ := ret[0].(*pmapi.Label)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLabel indicates an expected call of UpdateLabel
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) UpdateLabel(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).UpdateLabel), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUser mocks base method
|
|
||||||
func (m *MockPMAPIProvider) UpdateUser() (*pmapi.User, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdateUser")
|
|
||||||
ret0, _ := ret[0].(*pmapi.User)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUser indicates an expected call of UpdateUser
|
|
||||||
func (mr *MockPMAPIProviderMockRecorder) UpdateUser() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockPMAPIProvider)(nil).UpdateUser))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCredentialsStorer is a mock of CredentialsStorer interface
|
|
||||||
type MockCredentialsStorer struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockCredentialsStorerMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCredentialsStorerMockRecorder is the mock recorder for MockCredentialsStorer
|
|
||||||
type MockCredentialsStorerMockRecorder struct {
|
|
||||||
mock *MockCredentialsStorer
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockCredentialsStorer creates a new mock instance
|
|
||||||
func NewMockCredentialsStorer(ctrl *gomock.Controller) *MockCredentialsStorer {
|
|
||||||
mock := &MockCredentialsStorer{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockCredentialsStorerMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
|
||||||
func (m *MockCredentialsStorer) EXPECT() *MockCredentialsStorerMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add mocks base method
|
|
||||||
func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3 string, arg4 []string) (*credentials.Credentials, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Add", arg0, arg1, arg2, arg3, arg4)
|
|
||||||
ret0, _ := ret[0].(*credentials.Credentials)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add indicates an expected call of Add
|
|
||||||
func (mr *MockCredentialsStorerMockRecorder) Add(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockCredentialsStorer)(nil).Add), arg0, arg1, arg2, arg3, arg4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete mocks base method
|
|
||||||
func (m *MockCredentialsStorer) Delete(arg0 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Delete", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete indicates an expected call of Delete
|
|
||||||
func (mr *MockCredentialsStorerMockRecorder) Delete(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCredentialsStorer)(nil).Delete), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get mocks base method
|
|
||||||
func (m *MockCredentialsStorer) Get(arg0 string) (*credentials.Credentials, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Get", arg0)
|
|
||||||
ret0, _ := ret[0].(*credentials.Credentials)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get indicates an expected call of Get
|
|
||||||
func (mr *MockCredentialsStorerMockRecorder) Get(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCredentialsStorer)(nil).Get), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List mocks base method
|
|
||||||
func (m *MockCredentialsStorer) List() ([]string, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "List")
|
|
||||||
ret0, _ := ret[0].([]string)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// List indicates an expected call of List
|
|
||||||
func (mr *MockCredentialsStorerMockRecorder) List() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCredentialsStorer)(nil).List))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout mocks base method
|
|
||||||
func (m *MockCredentialsStorer) Logout(arg0 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Logout", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout indicates an expected call of Logout
|
|
||||||
func (mr *MockCredentialsStorerMockRecorder) Logout(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockCredentialsStorer)(nil).Logout), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SwitchAddressMode mocks base method
|
|
||||||
func (m *MockCredentialsStorer) SwitchAddressMode(arg0 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SwitchAddressMode", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SwitchAddressMode indicates an expected call of SwitchAddressMode
|
|
||||||
func (mr *MockCredentialsStorerMockRecorder) SwitchAddressMode(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwitchAddressMode", reflect.TypeOf((*MockCredentialsStorer)(nil).SwitchAddressMode), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateEmails mocks base method
|
|
||||||
func (m *MockCredentialsStorer) UpdateEmails(arg0 string, arg1 []string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdateEmails", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateEmails indicates an expected call of UpdateEmails
|
|
||||||
func (mr *MockCredentialsStorerMockRecorder) UpdateEmails(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmails", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateEmails), arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateToken mocks base method
|
|
||||||
func (m *MockCredentialsStorer) UpdateToken(arg0, arg1 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdateToken", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateToken indicates an expected call of UpdateToken
|
|
||||||
func (mr *MockCredentialsStorerMockRecorder) UpdateToken(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateToken", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateToken), arg0, arg1)
|
|
||||||
}
|
|
||||||
@ -15,12 +15,21 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Code generated by ./release-notes.sh at Thu 21 May 2020 07:59:59 AM CEST. DO NOT EDIT.
|
// Code generated by ./release-notes.sh at 'Mon Sep 21 01:29:10 PM CEST 2020'. DO NOT EDIT.
|
||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
const ReleaseNotes = `
|
const ReleaseNotes = `• Bulletproofing against any potential data loss and/or duplication
|
||||||
|
• Performance improvements for handling attachments and non-standard formatting
|
||||||
|
• Better stability of the message parser
|
||||||
|
• Additional foreign encoding support for outgoing messages
|
||||||
|
• Complete refactor of the way messages are parsed to simplify code maintenance
|
||||||
|
• Improved User-Agent detection
|
||||||
|
• Added MacOS Big Sur compatibility
|
||||||
|
• Added persistent anonymous API cookies
|
||||||
`
|
`
|
||||||
|
|
||||||
const ReleaseFixedBugs = `• Fixed ignored import to Sent folder
|
const ReleaseFixedBugs = `• Fixed rare mail loss when moving from Spam folder
|
||||||
|
• Limited log size
|
||||||
|
• Fixed Linux font issues (mouse hover).
|
||||||
`
|
`
|
||||||
|
|||||||
69
internal/bridge/store_factory.go
Normal file
69
internal/bridge/store_factory.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// 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 bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||||
|
)
|
||||||
|
|
||||||
|
type storeFactory struct {
|
||||||
|
config StoreFactoryConfiger
|
||||||
|
panicHandler users.PanicHandler
|
||||||
|
clientManager users.ClientManager
|
||||||
|
eventListener listener.Listener
|
||||||
|
storeCache *store.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStoreFactory(
|
||||||
|
config StoreFactoryConfiger,
|
||||||
|
panicHandler users.PanicHandler,
|
||||||
|
clientManager users.ClientManager,
|
||||||
|
eventListener listener.Listener,
|
||||||
|
) *storeFactory {
|
||||||
|
return &storeFactory{
|
||||||
|
config: config,
|
||||||
|
panicHandler: panicHandler,
|
||||||
|
clientManager: clientManager,
|
||||||
|
eventListener: eventListener,
|
||||||
|
storeCache: store.NewCache(config.GetIMAPCachePath()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates new store for given user.
|
||||||
|
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
|
||||||
|
storePath := getUserStorePath(f.config.GetDBDir(), user.ID())
|
||||||
|
return store.New(f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes all store files for given user.
|
||||||
|
func (f *storeFactory) Remove(userID string) error {
|
||||||
|
storePath := getUserStorePath(f.config.GetDBDir(), userID)
|
||||||
|
return store.RemoveStore(f.storeCache, storePath, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserStorePath returns the file path of the store database for the given userID.
|
||||||
|
func getUserStorePath(storeDir string, userID string) (path string) {
|
||||||
|
fileName := fmt.Sprintf("mailbox-%v.db", userID)
|
||||||
|
return filepath.Join(storeDir, fileName)
|
||||||
|
}
|
||||||
@ -17,89 +17,22 @@
|
|||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import "github.com/ProtonMail/proton-bridge/internal/users"
|
||||||
"io"
|
|
||||||
|
|
||||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
|
|
||||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" // mockgen needs this to be given an explicit import name
|
|
||||||
)
|
|
||||||
|
|
||||||
type Configer interface {
|
type Configer interface {
|
||||||
ClearData() error
|
users.Configer
|
||||||
|
StoreFactoryConfiger
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreFactoryConfiger interface {
|
||||||
GetDBDir() string
|
GetDBDir() string
|
||||||
GetIMAPCachePath() string
|
GetIMAPCachePath() string
|
||||||
GetAPIConfig() *pmapi.ClientConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreferenceProvider interface {
|
type PreferenceProvider interface {
|
||||||
Get(key string) string
|
Get(key string) string
|
||||||
GetBool(key string) bool
|
GetBool(key string) bool
|
||||||
|
SetBool(key string, val bool)
|
||||||
GetInt(key string) int
|
GetInt(key string) int
|
||||||
Set(key string, value string)
|
Set(key string, value string)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PanicHandler interface {
|
|
||||||
HandlePanic()
|
|
||||||
}
|
|
||||||
|
|
||||||
type PMAPIProviderFactory func(string) PMAPIProvider
|
|
||||||
|
|
||||||
type PMAPIProvider interface {
|
|
||||||
SetAuths(auths chan<- *pmapi.Auth)
|
|
||||||
Auth(username, password string, info *pmapi.AuthInfo) (*pmapi.Auth, error)
|
|
||||||
AuthInfo(username string) (*pmapi.AuthInfo, error)
|
|
||||||
AuthRefresh(token string) (*pmapi.Auth, error)
|
|
||||||
Unlock(mailboxPassword string) (kr *pmcrypto.KeyRing, err error)
|
|
||||||
UnlockAddresses(passphrase []byte) error
|
|
||||||
CurrentUser() (*pmapi.User, error)
|
|
||||||
UpdateUser() (*pmapi.User, error)
|
|
||||||
Addresses() pmapi.AddressList
|
|
||||||
Logout() error
|
|
||||||
|
|
||||||
GetEvent(eventID string) (*pmapi.Event, error)
|
|
||||||
|
|
||||||
CountMessages(addressID string) ([]*pmapi.MessagesCount, error)
|
|
||||||
ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Message, int, error)
|
|
||||||
GetMessage(apiID string) (*pmapi.Message, error)
|
|
||||||
Import([]*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error)
|
|
||||||
DeleteMessages(apiIDs []string) error
|
|
||||||
LabelMessages(apiIDs []string, labelID string) error
|
|
||||||
UnlabelMessages(apiIDs []string, labelID string) error
|
|
||||||
MarkMessagesRead(apiIDs []string) error
|
|
||||||
MarkMessagesUnread(apiIDs []string) error
|
|
||||||
|
|
||||||
ListLabels() ([]*pmapi.Label, error)
|
|
||||||
CreateLabel(label *pmapi.Label) (*pmapi.Label, error)
|
|
||||||
UpdateLabel(label *pmapi.Label) (*pmapi.Label, error)
|
|
||||||
DeleteLabel(labelID string) error
|
|
||||||
EmptyFolder(labelID string, addressID string) error
|
|
||||||
|
|
||||||
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
|
|
||||||
SendSimpleMetric(category, action, label string) error
|
|
||||||
|
|
||||||
Auth2FA(twoFactorCode string, auth *pmapi.Auth) (*pmapi.Auth2FA, error)
|
|
||||||
|
|
||||||
GetMailSettings() (pmapi.MailSettings, error)
|
|
||||||
GetContactEmailByEmail(string, int, int) ([]pmapi.ContactEmail, error)
|
|
||||||
GetContactByID(string) (pmapi.Contact, error)
|
|
||||||
DecryptAndVerifyCards([]pmapi.Card) ([]pmapi.Card, error)
|
|
||||||
GetPublicKeysForEmail(string) ([]pmapi.PublicKey, bool, error)
|
|
||||||
SendMessage(string, *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err error)
|
|
||||||
CreateDraft(m *pmapi.Message, parent string, action int) (created *pmapi.Message, err error)
|
|
||||||
CreateAttachment(att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error)
|
|
||||||
KeyRingForAddressID(string) (kr *pmcrypto.KeyRing)
|
|
||||||
|
|
||||||
GetAttachment(id string) (att io.ReadCloser, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CredentialsStorer interface {
|
|
||||||
List() (userIDs []string, err error)
|
|
||||||
Add(userID, userName, apiToken, mailboxPassword string, emails []string) (*credentials.Credentials, error)
|
|
||||||
Get(userID string) (*credentials.Credentials, error)
|
|
||||||
SwitchAddressMode(userID string) error
|
|
||||||
UpdateEmails(userID string, emails []string) error
|
|
||||||
UpdateToken(userID, apiToken string) error
|
|
||||||
Logout(userID string) error
|
|
||||||
Delete(userID string) error
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,188 +0,0 @@
|
|||||||
// 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 bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
a "github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewUserNoCredentialsStore(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("fail"))
|
|
||||||
|
|
||||||
_, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
|
||||||
a.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewUserBridgeOutdated(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrUpgradeApplication).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
|
||||||
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "").AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrUpgradeApplication)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
|
||||||
|
|
||||||
checkNewUser(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewUserNoInternetConnection(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
|
||||||
m.eventListener.EXPECT().Emit(events.InternetOffEvent, "").AnyTimes()
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrAPINotReachable)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
|
|
||||||
|
|
||||||
checkNewUser(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewUserAuthRefreshFails(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token")).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
|
||||||
|
|
||||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
|
||||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
|
||||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(nil)
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
|
||||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
|
||||||
|
|
||||||
checkNewUserDisconnected(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewUserUnlockFails(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, errors.New("bad password"))
|
|
||||||
|
|
||||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
|
||||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
|
||||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(nil)
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
|
||||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
|
||||||
|
|
||||||
checkNewUserDisconnected(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewUserUnlockAddressesFails(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(errors.New("bad password"))
|
|
||||||
|
|
||||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
|
||||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
|
||||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(nil)
|
|
||||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
|
||||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
|
||||||
|
|
||||||
checkNewUserDisconnected(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewUser(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
|
||||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
|
||||||
|
|
||||||
checkNewUser(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkNewUser(m mocks) {
|
|
||||||
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
|
||||||
defer cleanUpUserData(user)
|
|
||||||
|
|
||||||
_ = user.init(nil, m.pmapiClient)
|
|
||||||
|
|
||||||
waitForEvents()
|
|
||||||
|
|
||||||
a.Equal(m.t, testCredentials, user.creds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkNewUserDisconnected(m mocks) {
|
|
||||||
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
|
||||||
defer cleanUpUserData(user)
|
|
||||||
|
|
||||||
_ = user.init(nil, m.pmapiClient)
|
|
||||||
|
|
||||||
waitForEvents()
|
|
||||||
|
|
||||||
a.Equal(m.t, testCredentialsDisconnected, user.creds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _TestUserEventRefreshUpdatesAddresses(t *testing.T) { // nolint[funlen]
|
|
||||||
a.Fail(t, "not implemented")
|
|
||||||
}
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
// 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 bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testNewUser(m mocks) *User {
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
|
|
||||||
|
|
||||||
// Expectations for initial sync (when loading existing user from credentials store).
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
|
||||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
|
|
||||||
|
|
||||||
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
|
||||||
assert.NoError(m.t, err)
|
|
||||||
|
|
||||||
err = user.init(nil, m.pmapiClient)
|
|
||||||
assert.NoError(m.t, err)
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func testNewUserForLogout(m mocks) *User {
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
|
||||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
|
||||||
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
|
||||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
|
||||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
|
||||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
|
||||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
|
|
||||||
|
|
||||||
// These may or may not be hit depending on how fast the log out happens.
|
|
||||||
m.pmapiClient.EXPECT().SetAuths(nil).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
|
||||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
|
|
||||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
|
|
||||||
|
|
||||||
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
|
||||||
assert.NoError(m.t, err)
|
|
||||||
|
|
||||||
err = user.init(nil, m.pmapiClient)
|
|
||||||
assert.NoError(m.t, err)
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanUpUserData(u *User) {
|
|
||||||
_ = u.clearStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
func _TestNeverLongStorePath(t *testing.T) { // nolint[unused]
|
|
||||||
assert.Fail(t, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClearStoreWithStore(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
user := testNewUserForLogout(m)
|
|
||||||
defer cleanUpUserData(user)
|
|
||||||
|
|
||||||
require.Nil(t, user.store.Close())
|
|
||||||
user.store = nil
|
|
||||||
assert.Nil(t, user.clearStore())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClearStoreWithoutStore(t *testing.T) {
|
|
||||||
m := initMocks(t)
|
|
||||||
defer m.ctrl.Finish()
|
|
||||||
|
|
||||||
user := testNewUserForLogout(m)
|
|
||||||
defer cleanUpUserData(user)
|
|
||||||
|
|
||||||
assert.NotNil(t, user.store)
|
|
||||||
assert.Nil(t, user.clearStore())
|
|
||||||
}
|
|
||||||
@ -15,16 +15,16 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package args
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"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
|
// 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]
|
tmp := os.Args[:0]
|
||||||
for _, arg := range os.Args {
|
for _, arg := range os.Args {
|
||||||
if !strings.Contains(arg, "-psn_") {
|
if !strings.Contains(arg, "-psn_") {
|
||||||
86
internal/cmd/main.go
Normal file
86
internal/cmd/main.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) 2020 Proton Technologies AG
|
||||||
|
//
|
||||||
|
// This file is part of ProtonMail Bridge.
|
||||||
|
//
|
||||||
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||||
|
"github.com/getsentry/raven-go"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
log = logrus.WithField("pkg", "cmd") //nolint[gochecknoglobals]
|
||||||
|
|
||||||
|
baseFlags = []cli.Flag{ //nolint[gochecknoglobals]
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "log-level, l",
|
||||||
|
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "cli, c",
|
||||||
|
Usage: "Use command line interface"},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "version-json, g",
|
||||||
|
Usage: "Generate json version file"},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "mem-prof, m",
|
||||||
|
Usage: "Generate memory profile"},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "cpu-prof, p",
|
||||||
|
Usage: "Generate CPU profile"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Main sets up Sentry, filters out unwanted args, creates app and runs it.
|
||||||
|
func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) {
|
||||||
|
if err := raven.SetDSN(constants.DSNSentry); err != nil {
|
||||||
|
log.WithError(err).Errorln("Can not setup sentry DSN")
|
||||||
|
}
|
||||||
|
raven.SetRelease(constants.Revision)
|
||||||
|
|
||||||
|
filterProcessSerialNumberFromArgs()
|
||||||
|
filterRestartNumberFromArgs()
|
||||||
|
|
||||||
|
app := newApp(appName, usage, extraFlags, run)
|
||||||
|
|
||||||
|
logrus.SetLevel(logrus.InfoLevel)
|
||||||
|
log.WithField("version", constants.Version).
|
||||||
|
WithField("revision", constants.Revision).
|
||||||
|
WithField("build", constants.BuildTime).
|
||||||
|
WithField("runtime", runtime.GOOS).
|
||||||
|
WithField("args", os.Args).
|
||||||
|
WithField("appName", app.Name).
|
||||||
|
Info("Run app")
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Error("Program exited with error: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApp(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) *cli.App {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = appName
|
||||||
|
app.Usage = usage
|
||||||
|
app.Version = constants.BuildVersion
|
||||||
|
app.Flags = append(baseFlags, extraFlags...) //nolint[gocritic]
|
||||||
|
app.Action = run
|
||||||
|
return app
|
||||||
|
}
|
||||||
54
internal/cmd/profiles.go
Normal file
54
internal/cmd/profiles.go
Normal 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
108
internal/cmd/restart.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2020 Proton Technologies AG
|
||||||
|
//
|
||||||
|
// This file is part of ProtonMail Bridge.
|
||||||
|
//
|
||||||
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// After how many crashes app gives up starting.
|
||||||
|
maxAllowedCrashes = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// How many crashes happened so far in a row.
|
||||||
|
// It will be filled from args by `filterRestartNumberFromArgs`.
|
||||||
|
// Every call of `HandlePanic` will increase this number.
|
||||||
|
// Then it will be passed as argument to the next try by `RestartApp`.
|
||||||
|
numberOfCrashes = 0 //nolint[gochecknoglobals]
|
||||||
|
)
|
||||||
|
|
||||||
|
// filterRestartNumberFromArgs removes flag with a number how many restart we already did.
|
||||||
|
// See restartApp how that number is used.
|
||||||
|
func filterRestartNumberFromArgs() {
|
||||||
|
tmp := os.Args[:0]
|
||||||
|
for i, arg := range os.Args {
|
||||||
|
if !strings.HasPrefix(arg, "--restart_") {
|
||||||
|
tmp = append(tmp, arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
numberOfCrashes, err = strconv.Atoi(os.Args[i][10:])
|
||||||
|
if err != nil {
|
||||||
|
numberOfCrashes = maxAllowedCrashes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Args = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableRestart disables restart once `RestartApp` is called.
|
||||||
|
func DisableRestart() {
|
||||||
|
numberOfCrashes = maxAllowedCrashes
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestartApp starts a new instance in background.
|
||||||
|
func RestartApp() {
|
||||||
|
if numberOfCrashes >= maxAllowedCrashes {
|
||||||
|
log.Error("Too many crashes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exeFile, err := os.Executable(); err == nil {
|
||||||
|
arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes))
|
||||||
|
cmd := exec.Command(exeFile, arguments...) //nolint[gosec]
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Error("Restart failed: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PanicHandler defines HandlePanic which can be used anywhere in defer.
|
||||||
|
type PanicHandler struct {
|
||||||
|
AppName string
|
||||||
|
Config *config.Config
|
||||||
|
Err *error // Pointer to error of cli action.
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePanic should be called in defer to ensure restart of app after error.
|
||||||
|
func (ph *PanicHandler) HandlePanic() {
|
||||||
|
r := recover()
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.HandlePanic(ph.Config, fmt.Sprintf("Recover: %v", r))
|
||||||
|
frontend.HandlePanic(ph.AppName)
|
||||||
|
|
||||||
|
*ph.Err = cli.NewExitError("Panic and restart", 255)
|
||||||
|
numberOfCrashes++
|
||||||
|
log.Error("Restarting after panic")
|
||||||
|
RestartApp()
|
||||||
|
os.Exit(255)
|
||||||
|
}
|
||||||
32
internal/cmd/version_file.go
Normal file
32
internal/cmd/version_file.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) 2020 Proton Technologies AG
|
||||||
|
//
|
||||||
|
// This file is part of ProtonMail Bridge.
|
||||||
|
//
|
||||||
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "github.com/ProtonMail/proton-bridge/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
92
internal/cookies/jar.go
Normal 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)
|
||||||
|
}
|
||||||
168
internal/cookies/jar_test.go
Normal file
168
internal/cookies/jar_test.go
Normal 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
100
internal/cookies/pantry.go
Normal 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
|
||||||
|
}
|
||||||
@ -40,6 +40,7 @@ const (
|
|||||||
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
|
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
|
||||||
UpgradeApplicationEvent = "upgradeApplication"
|
UpgradeApplicationEvent = "upgradeApplication"
|
||||||
TLSCertIssue = "tlsCertPinningIssue"
|
TLSCertIssue = "tlsCertPinningIssue"
|
||||||
|
IMAPTLSBadCert = "imapTLSBadCert"
|
||||||
|
|
||||||
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
|
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
|
||||||
LogoutEventTimeout = 3 * time.Minute
|
LogoutEventTimeout = 3 * time.Minute
|
||||||
|
|||||||
@ -43,7 +43,7 @@ func (c *appleMail) Name() string {
|
|||||||
return "Apple Mail"
|
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 addresses string
|
||||||
var displayName string
|
var displayName string
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import "github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
|||||||
|
|
||||||
type AutoConfig interface {
|
type AutoConfig interface {
|
||||||
Name() string
|
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]
|
var available []AutoConfig //nolint[gochecknoglobals]
|
||||||
|
|||||||
100
internal/frontend/cli-ie/account_utils.go
Normal file
100
internal/frontend/cli-ie/account_utils.go
Normal 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
|
||||||
|
}
|
||||||
153
internal/frontend/cli-ie/accounts.go
Normal file
153
internal/frontend/cli-ie/accounts.go
Normal 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")
|
||||||
|
}
|
||||||
237
internal/frontend/cli-ie/frontend.go
Normal file
237
internal/frontend/cli-ie/frontend.go
Normal 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
|
||||||
|
}
|
||||||
224
internal/frontend/cli-ie/importexport.go
Normal file
224
internal/frontend/cli-ie/importexport.go
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
46
internal/frontend/cli-ie/system.go
Normal file
46
internal/frontend/cli-ie/system.go
Normal 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/")
|
||||||
|
}
|
||||||
65
internal/frontend/cli-ie/updates.go
Normal file
65
internal/frontend/cli-ie/updates.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
123
internal/frontend/cli-ie/utils.go
Normal file
123
internal/frontend/cli-ie/utils.go
Normal 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.
|
||||||
|
`)
|
||||||
|
}
|
||||||
@ -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("")
|
user := f.getUserByIndexOrName("")
|
||||||
if user != nil {
|
if user != nil {
|
||||||
return user
|
return user
|
||||||
@ -76,7 +76,7 @@ func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *frontendCLI) getUserByIndexOrName(arg string) types.BridgeUser {
|
func (f *frontendCLI) getUserByIndexOrName(arg string) types.User {
|
||||||
users := f.bridge.GetUsers()
|
users := f.bridge.GetUsers()
|
||||||
numberOfAccounts := len(users)
|
numberOfAccounts := len(users)
|
||||||
if numberOfAccounts == 0 {
|
if numberOfAccounts == 0 {
|
||||||
|
|||||||
@ -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"
|
smtpSecurity := "STARTTLS"
|
||||||
if f.preferences.GetBool(preferences.SMTPSSLKey) {
|
if f.preferences.GetBool(preferences.SMTPSSLKey) {
|
||||||
smtpSecurity = "SSL"
|
smtpSecurity = "SSL"
|
||||||
@ -126,7 +126,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.Auth2FA(twoFactor, auth)
|
err = client.Auth2FA(twoFactor, auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.processAPIError(err)
|
f.processAPIError(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -21,15 +21,15 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
"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/config"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||||
|
|
||||||
"github.com/abiosoft/ishell"
|
"github.com/abiosoft/ishell"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log = config.GetLogEntry("frontend/cli") //nolint[gochecknoglobals]
|
log = logrus.WithField("pkg", "frontend/cli") //nolint[gochecknoglobals]
|
||||||
)
|
)
|
||||||
|
|
||||||
type frontendCLI struct {
|
type frontendCLI struct {
|
||||||
@ -76,7 +76,7 @@ func New( //nolint[funlen]
|
|||||||
Func: fe.deleteCache,
|
Func: fe.deleteCache,
|
||||||
})
|
})
|
||||||
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
|
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"},
|
Aliases: []string{"a", "k", "keychain"},
|
||||||
Func: fe.deleteAccounts,
|
Func: fe.deleteAccounts,
|
||||||
})
|
})
|
||||||
@ -240,8 +240,6 @@ func (f *frontendCLI) Loop(credentialsError error) error {
|
|||||||
return credentialsError
|
return credentialsError
|
||||||
}
|
}
|
||||||
|
|
||||||
f.preferences.SetBool(preferences.FirstStartKey, false)
|
|
||||||
|
|
||||||
f.Print(`
|
f.Print(`
|
||||||
Welcome to ProtonMail Bridge interactive shell
|
Welcome to ProtonMail Bridge interactive shell
|
||||||
___....___
|
___....___
|
||||||
|
|||||||
@ -22,9 +22,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/connection"
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||||
"github.com/abiosoft/ishell"
|
"github.com/abiosoft/ishell"
|
||||||
)
|
)
|
||||||
@ -42,10 +40,10 @@ func (f *frontendCLI) restart(c *ishell.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
||||||
if connection.CheckInternetConnection() == nil {
|
if f.bridge.CheckConnection() == nil {
|
||||||
f.Println("Internet connection is available.")
|
f.Println("Internet connection is available.")
|
||||||
} else {
|
} else {
|
||||||
f.Println("Can not contact server please check you internet connection.")
|
f.Println("Can not contact the server, please check your internet connection.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,13 +133,13 @@ func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
|
|||||||
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
|
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
|
||||||
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
|
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
|
||||||
f.preferences.SetBool(preferences.AllowProxyKey, false)
|
f.preferences.SetBool(preferences.AllowProxyKey, false)
|
||||||
bridge.DisallowDoH()
|
f.bridge.DisallowProxy()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
|
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
|
||||||
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
|
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
|
||||||
f.preferences.SetBool(preferences.AllowProxyKey, true)
|
f.preferences.SetBool(preferences.AllowProxyKey, true)
|
||||||
bridge.AllowDoH()
|
f.bridge.AllowProxy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,12 +21,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
"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"
|
"github.com/abiosoft/ishell"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
|
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
|
||||||
isUpToDate, latestVersionInfo, err := f.updates.CheckIsBridgeUpToDate()
|
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.printAndLogError("Cannot retrieve version info: ", err)
|
f.printAndLogError("Cannot retrieve version info: ", err)
|
||||||
f.checkInternetConnection(c)
|
f.checkInternetConnection(c)
|
||||||
|
|||||||
@ -22,14 +22,18 @@ import (
|
|||||||
"github.com/0xAX/notificator"
|
"github.com/0xAX/notificator"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
|
"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"
|
"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/frontend/types"
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log = config.GetLogEntry("frontend") // nolint[unused]
|
log = logrus.WithField("pkg", "frontend") // nolint[unused]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Frontend is an interface to be implemented by each frontend type (cli, gui, html).
|
// Frontend is an interface to be implemented by each frontend type (cli, gui, html).
|
||||||
@ -39,12 +43,12 @@ type Frontend interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandlePanic handles panics which occur for users with GUI.
|
// HandlePanic handles panics which occur for users with GUI.
|
||||||
func HandlePanic() {
|
func HandlePanic(appName string) {
|
||||||
notify := notificator.New(notificator.Options{
|
notify := notificator.New(notificator.Options{
|
||||||
DefaultIcon: "../frontend/ui/icon/icon.png",
|
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`.
|
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
|
||||||
@ -85,3 +89,36 @@ func new(
|
|||||||
return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -354,7 +354,7 @@ Window {
|
|||||||
} else {
|
} else {
|
||||||
return qsTr('A new version of Bridge is available.<br>
|
return qsTr('A new version of Bridge is available.<br>
|
||||||
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
|
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
|
||||||
You can continue with the update or download and install new version manually from<br><br>
|
You can continue with the update or download and install the new version manually from<br><br>
|
||||||
<a href="%3">%3</a>',
|
<a href="%3">%3</a>',
|
||||||
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,7 +84,7 @@ Window {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
ButtonRounded {
|
ButtonRounded {
|
||||||
id: cancel
|
id: sendAnyway
|
||||||
onClicked : root.hide(true)
|
onClicked : root.hide(true)
|
||||||
height: Style.main.fontSize*2
|
height: Style.main.fontSize*2
|
||||||
//width: Style.dialog.widthButton*1.3
|
//width: Style.dialog.widthButton*1.3
|
||||||
@ -93,7 +93,7 @@ Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ButtonRounded {
|
ButtonRounded {
|
||||||
id: sendAnyway
|
id: cancel
|
||||||
onClicked : root.hide(false)
|
onClicked : root.hide(false)
|
||||||
height: Style.main.fontSize*2
|
height: Style.main.fontSize*2
|
||||||
//width: Style.dialog.widthButton*1.3
|
//width: Style.dialog.widthButton*1.3
|
||||||
|
|||||||
@ -237,6 +237,14 @@ Item {
|
|||||||
winMain.tlsBarState="notOK"
|
winMain.tlsBarState="notOK"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onShowIMAPCertTroubleshoot : {
|
||||||
|
go.notifyBubble(1, qsTr(
|
||||||
|
"Bridge was unable to establish a connection with your Email client. <br> <a href=\"https://protonmail.com/support/knowledge-base/bridge-ssl-connection-issue\">Learn more</a> <br>",
|
||||||
|
"notification message"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
|
|||||||
418
internal/frontend/qml/GuiIE.qml
Normal file
418
internal/frontend/qml/GuiIE.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
432
internal/frontend/qml/ImportExportUI/AccountDelegate.qml
Normal file
432
internal/frontend/qml/ImportExportUI/AccountDelegate.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
51
internal/frontend/qml/ImportExportUI/Credits.qml
Normal file
51
internal/frontend/qml/ImportExportUI/Credits.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
internal/frontend/qml/ImportExportUI/DateBox.qml
Normal file
220
internal/frontend/qml/ImportExportUI/DateBox.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
264
internal/frontend/qml/ImportExportUI/DateInput.qml
Normal file
264
internal/frontend/qml/ImportExportUI/DateInput.qml
Normal 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)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
123
internal/frontend/qml/ImportExportUI/DateRange.qml
Normal file
123
internal/frontend/qml/ImportExportUI/DateRange.qml
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
internal/frontend/qml/ImportExportUI/DateRangeFunctions.qml
Normal file
81
internal/frontend/qml/ImportExportUI/DateRangeFunctions.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
163
internal/frontend/qml/ImportExportUI/DateRangeMenu.qml
Normal file
163
internal/frontend/qml/ImportExportUI/DateRangeMenu.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
457
internal/frontend/qml/ImportExportUI/DialogExport.qml
Normal file
457
internal/frontend/qml/ImportExportUI/DialogExport.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1016
internal/frontend/qml/ImportExportUI/DialogImport.qml
Normal file
1016
internal/frontend/qml/ImportExportUI/DialogImport.qml
Normal file
File diff suppressed because it is too large
Load Diff
354
internal/frontend/qml/ImportExportUI/DialogYesNo.qml
Normal file
354
internal/frontend/qml/ImportExportUI/DialogYesNo.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
internal/frontend/qml/ImportExportUI/ExportStructure.qml
Normal file
149
internal/frontend/qml/ImportExportUI/ExportStructure.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
internal/frontend/qml/ImportExportUI/FilterStructure.qml
Normal file
55
internal/frontend/qml/ImportExportUI/FilterStructure.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
internal/frontend/qml/ImportExportUI/FolderRowButton.qml
Normal file
99
internal/frontend/qml/ImportExportUI/FolderRowButton.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
internal/frontend/qml/ImportExportUI/HelpView.qml
Normal file
129
internal/frontend/qml/ImportExportUI/HelpView.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
106
internal/frontend/qml/ImportExportUI/IEStyle.qml
Normal file
106
internal/frontend/qml/ImportExportUI/IEStyle.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
154
internal/frontend/qml/ImportExportUI/ImportDelegate.qml
Normal file
154
internal/frontend/qml/ImportExportUI/ImportDelegate.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
216
internal/frontend/qml/ImportExportUI/ImportReport.qml
Normal file
216
internal/frontend/qml/ImportExportUI/ImportReport.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
67
internal/frontend/qml/ImportExportUI/ImportReportCell.qml
Normal file
67
internal/frontend/qml/ImportExportUI/ImportReportCell.qml
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
89
internal/frontend/qml/ImportExportUI/ImportSourceButton.qml
Normal file
89
internal/frontend/qml/ImportExportUI/ImportSourceButton.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
148
internal/frontend/qml/ImportExportUI/ImportStructure.qml
Normal file
148
internal/frontend/qml/ImportExportUI/ImportStructure.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
129
internal/frontend/qml/ImportExportUI/InlineDateRange.qml
Normal file
129
internal/frontend/qml/ImportExportUI/InlineDateRange.qml
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
227
internal/frontend/qml/ImportExportUI/InlineLabelSelect.qml
Normal file
227
internal/frontend/qml/ImportExportUI/InlineLabelSelect.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
65
internal/frontend/qml/ImportExportUI/LabelIconList.qml
Normal file
65
internal/frontend/qml/ImportExportUI/LabelIconList.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
475
internal/frontend/qml/ImportExportUI/MainWindow.qml
Normal file
475
internal/frontend/qml/ImportExportUI/MainWindow.qml
Normal 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('A 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 the 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('A 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 the 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
84
internal/frontend/qml/ImportExportUI/OutputFormat.qml
Normal file
84
internal/frontend/qml/ImportExportUI/OutputFormat.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
311
internal/frontend/qml/ImportExportUI/PopupEditFolder.qml
Normal file
311
internal/frontend/qml/ImportExportUI/PopupEditFolder.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
339
internal/frontend/qml/ImportExportUI/SelectFolderMenu.qml
Normal file
339
internal/frontend/qml/ImportExportUI/SelectFolderMenu.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
29
internal/frontend/qml/ImportExportUI/SelectLabelsMenu.qml
Normal file
29
internal/frontend/qml/ImportExportUI/SelectLabelsMenu.qml
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
148
internal/frontend/qml/ImportExportUI/SettingsView.qml
Normal file
148
internal/frontend/qml/ImportExportUI/SettingsView.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
125
internal/frontend/qml/ImportExportUI/VersionInfo.qml
Normal file
125
internal/frontend/qml/ImportExportUI/VersionInfo.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
31
internal/frontend/qml/ImportExportUI/qmldir
Normal file
31
internal/frontend/qml/ImportExportUI/qmldir
Normal 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
|
||||||
@ -87,7 +87,7 @@ Item {
|
|||||||
Text { // Status
|
Text { // Status
|
||||||
anchors {
|
anchors {
|
||||||
left : parent.left
|
left : parent.left
|
||||||
leftMargin : Style.accounts.leftMargin2
|
leftMargin : viewContent.width/2
|
||||||
verticalCenter : parent.verticalCenter
|
verticalCenter : parent.verticalCenter
|
||||||
}
|
}
|
||||||
visible: root.numAccounts!=0
|
visible: root.numAccounts!=0
|
||||||
@ -99,7 +99,7 @@ Item {
|
|||||||
Text { // Actions
|
Text { // Actions
|
||||||
anchors {
|
anchors {
|
||||||
left : parent.left
|
left : parent.left
|
||||||
leftMargin : Style.accounts.leftMargin3
|
leftMargin : 5.5*viewContent.width/8
|
||||||
verticalCenter : parent.verticalCenter
|
verticalCenter : parent.verticalCenter
|
||||||
}
|
}
|
||||||
visible: root.numAccounts!=0
|
visible: root.numAccounts!=0
|
||||||
|
|||||||
@ -106,6 +106,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: mainText
|
anchors.fill: mainText
|
||||||
|
cursorShape: mainText.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ Window {
|
|||||||
|
|
||||||
color : "transparent"
|
color : "transparent"
|
||||||
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
|
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
|
||||||
title : "ProtonMail Bridge - Bug report"
|
title : "Bug report"
|
||||||
visible : false
|
visible : false
|
||||||
|
|
||||||
WindowTitleBar {
|
WindowTitleBar {
|
||||||
@ -327,6 +327,7 @@ Window {
|
|||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
prefill()
|
prefill()
|
||||||
|
description.focus=true
|
||||||
root.visible=true
|
root.visible=true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,10 +30,12 @@ CheckBox {
|
|||||||
property color uncheckedColor : Style.main.textInactive
|
property color uncheckedColor : Style.main.textInactive
|
||||||
property string checkedSymbol : Style.fa.check_square_o
|
property string checkedSymbol : Style.fa.check_square_o
|
||||||
property string uncheckedSymbol : Style.fa.square_o
|
property string uncheckedSymbol : Style.fa.square_o
|
||||||
|
property alias symbolPointSize : symbol.font.pointSize
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Style.transparent
|
color: Style.transparent
|
||||||
}
|
}
|
||||||
indicator: Text {
|
indicator: Text {
|
||||||
|
id: symbol
|
||||||
text : root.checked ? root.checkedSymbol : root.uncheckedSymbol
|
text : root.checked ? root.checkedSymbol : root.uncheckedSymbol
|
||||||
color : root.checked ? root.checkedColor : root.uncheckedColor
|
color : root.checked ? root.checkedColor : root.uncheckedColor
|
||||||
font {
|
font {
|
||||||
|
|||||||
@ -123,12 +123,12 @@ Dialog {
|
|||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
text: {
|
text: {
|
||||||
switch (go.progressDescription) {
|
switch (go.progressDescription) {
|
||||||
case 1: return qsTr("Checking the current version.")
|
case "1": return qsTr("Checking the current version.")
|
||||||
case 2: return qsTr("Downloading the update files.")
|
case "2": return qsTr("Downloading the update files.")
|
||||||
case 3: return qsTr("Verifying the update files.")
|
case "3": return qsTr("Verifying the update files.")
|
||||||
case 4: return qsTr("Unpacking the update files.")
|
case "4": return qsTr("Unpacking the update files.")
|
||||||
case 5: return qsTr("Starting the update.")
|
case "5": return qsTr("Starting the update.")
|
||||||
case 6: return qsTr("Quitting the application.")
|
case "6": return qsTr("Quitting the application.")
|
||||||
default: return ""
|
default: return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -220,7 +220,7 @@ Dialog {
|
|||||||
function clear() {
|
function clear() {
|
||||||
root.hasError = false
|
root.hasError = false
|
||||||
go.progress = 0.0
|
go.progress = 0.0
|
||||||
go.progressDescription = 0
|
go.progressDescription = "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
function finished(hasError) {
|
function finished(hasError) {
|
||||||
|
|||||||
@ -60,7 +60,7 @@ Row {
|
|||||||
|
|
||||||
FileDialog {
|
FileDialog {
|
||||||
id: pathDialog
|
id: pathDialog
|
||||||
title: root.title + ":"
|
title: root.title
|
||||||
folder: shortcuts.home
|
folder: shortcuts.home
|
||||||
onAccepted: sanitizePath(pathDialog.fileUrl.toString())
|
onAccepted: sanitizePath(pathDialog.fileUrl.toString())
|
||||||
selectFolder: true
|
selectFolder: true
|
||||||
|
|||||||
@ -138,6 +138,11 @@ Column {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
inputField.text = ""
|
||||||
|
rightIcon = ""
|
||||||
|
}
|
||||||
|
|
||||||
function checkNonEmpty() {
|
function checkNonEmpty() {
|
||||||
if (inputField.text == "") {
|
if (inputField.text == "") {
|
||||||
rightIcon = Style.fa.exclamation_triangle
|
rightIcon = Style.fa.exclamation_triangle
|
||||||
@ -154,6 +159,17 @@ Column {
|
|||||||
if (root.isPassword) inputField.echoMode = TextInput.Password
|
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() {
|
function forceFocus() {
|
||||||
inputField.forceActiveFocus()
|
inputField.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,26 @@ import ProtonUI 1.0
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
color: Style.transparent
|
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
|
visible: false
|
||||||
|
|
||||||
|
signal clickedOkay()
|
||||||
|
signal clickedYes()
|
||||||
|
signal clickedNo()
|
||||||
|
signal clickedRetry()
|
||||||
|
signal clickedSkip()
|
||||||
|
signal clickedCancel()
|
||||||
|
|
||||||
MouseArea { // prevent action below
|
MouseArea { // prevent action below
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
@ -58,14 +75,29 @@ Rectangle {
|
|||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonRounded {
|
CheckBoxLabel {
|
||||||
text : qsTr("Okay", "todo")
|
id: checkbox
|
||||||
isOpaque : true
|
text: ""
|
||||||
color_main : Style.dialog.background
|
checked: false
|
||||||
color_minor : Style.dialog.textBlue
|
visible: (text != "")
|
||||||
onClicked : root.hide()
|
textColor : Style.errorDialog.text
|
||||||
|
checkedColor: Style.errorDialog.text
|
||||||
|
uncheckedColor: Style.errorDialog.text
|
||||||
anchors.horizontalCenter : parent.horizontalCenter
|
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() {
|
function hide() {
|
||||||
root.state = "Okay"
|
|
||||||
root.visible=false
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
internal/frontend/qml/ProtonUI/RoundedRectangle.qml
Normal file
115
internal/frontend/qml/ProtonUI/RoundedRectangle.qml
Normal 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()
|
||||||
|
}
|
||||||
@ -221,6 +221,31 @@ QtObject {
|
|||||||
property real leftMargin3 : 30 * px
|
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 okInfoBar : 0
|
||||||
property int warnInfoBar : 1
|
property int warnInfoBar : 1
|
||||||
property int warnBubbleMessage : 2
|
property int warnBubbleMessage : 2
|
||||||
|
|||||||
@ -23,7 +23,9 @@ import ProtonUI 1.0
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
height: root.isDarwin ? Style.titleMacOS.height : Style.title.height
|
height: visible ? (
|
||||||
|
root.isDarwin ? Style.titleMacOS.height : Style.title.height
|
||||||
|
) : 0
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
property bool isDarwin : (go.goos == "darwin")
|
property bool isDarwin : (go.goos == "darwin")
|
||||||
property QtObject window
|
property QtObject window
|
||||||
|
|||||||
@ -23,6 +23,7 @@ InputField 1.0 InputField.qml
|
|||||||
InstanceExistsWindow 1.0 InstanceExistsWindow.qml
|
InstanceExistsWindow 1.0 InstanceExistsWindow.qml
|
||||||
LogoHeader 1.0 LogoHeader.qml
|
LogoHeader 1.0 LogoHeader.qml
|
||||||
PopupMessage 1.0 PopupMessage.qml
|
PopupMessage 1.0 PopupMessage.qml
|
||||||
|
RoundedRectangle 1.0 RoundedRectangle.qml
|
||||||
TabButton 1.0 TabButton.qml
|
TabButton 1.0 TabButton.qml
|
||||||
TabLabels 1.0 TabLabels.qml
|
TabLabels 1.0 TabLabels.qml
|
||||||
TextLabel 1.0 TextLabel.qml
|
TextLabel 1.0 TextLabel.qml
|
||||||
|
|||||||
@ -110,7 +110,7 @@ Window {
|
|||||||
ListElement { title: "Internet off" }
|
ListElement { title: "Internet off" }
|
||||||
ListElement { title: "NeedUpdate" }
|
ListElement { title: "NeedUpdate" }
|
||||||
ListElement { title: "UpToDate" }
|
ListElement { title: "UpToDate" }
|
||||||
ListElement { title: "ForceUpdate" }
|
ListElement { title: "ForceUpdate" }
|
||||||
ListElement { title: "Linux" }
|
ListElement { title: "Linux" }
|
||||||
ListElement { title: "Windows" }
|
ListElement { title: "Windows" }
|
||||||
ListElement { title: "Macos" }
|
ListElement { title: "Macos" }
|
||||||
@ -122,6 +122,7 @@ Window {
|
|||||||
ListElement { title: "Minimize this" }
|
ListElement { title: "Minimize this" }
|
||||||
ListElement { title: "SendAlertPopup" }
|
ListElement { title: "SendAlertPopup" }
|
||||||
ListElement { title: "TLSCertError" }
|
ListElement { title: "TLSCertError" }
|
||||||
|
ListElement { title: "IMAPCertError" }
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
@ -208,6 +209,9 @@ Window {
|
|||||||
case "TLSCertError" :
|
case "TLSCertError" :
|
||||||
go.showCertIssue()
|
go.showCertIssue()
|
||||||
break;
|
break;
|
||||||
|
case "IMAPCertError" :
|
||||||
|
go.showIMAPCertTroubleshoot()
|
||||||
|
break;
|
||||||
default :
|
default :
|
||||||
console.log("Not implemented " + data)
|
console.log("Not implemented " + data)
|
||||||
}
|
}
|
||||||
@ -310,6 +314,7 @@ Window {
|
|||||||
signal failedAutostartCode(string code)
|
signal failedAutostartCode(string code)
|
||||||
|
|
||||||
signal showCertIssue()
|
signal showCertIssue()
|
||||||
|
signal showIMAPCertTroubleshoot()
|
||||||
|
|
||||||
signal updateFinished(bool hasError)
|
signal updateFinished(bool hasError)
|
||||||
|
|
||||||
|
|||||||
1329
internal/frontend/qml/tst_GuiIE.qml
Normal file
1329
internal/frontend/qml/tst_GuiIE.qml
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user