mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
410 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df318382b7 | |||
| 341487d839 | |||
| 7468ed7dc0 | |||
| c6107dbd4b | |||
| bcef1c36ba | |||
| 6d9d5f35ca | |||
| 6299a6d390 | |||
| 4ab5635293 | |||
| 5c9e9caa2f | |||
| e055acb8eb | |||
| 72c01046e3 | |||
| 46bc8b08dc | |||
| 00b5046653 | |||
| 21d8ef649f | |||
| 6c96643d12 | |||
| 9193205834 | |||
| 15df130d76 | |||
| 4554369292 | |||
| 50d167a983 | |||
| 7065211064 | |||
| 0069eb9a0c | |||
| 35b5b925cf | |||
| d4df2ea348 | |||
| 0159f24f17 | |||
| 619d5eaec9 | |||
| 52804c7039 | |||
| 837e0d3758 | |||
| fa3829cbfe | |||
| 0679b99a65 | |||
| 4ffa62f6ca | |||
| 0c458f709f | |||
| d1daa02b35 | |||
| 26cdfdeba9 | |||
| f4405b5186 | |||
| 76dda10572 | |||
| 0cde1ab801 | |||
| d9c9edf4d7 | |||
| 29f034abdc | |||
| 62a64cde61 | |||
| 9747145a3c | |||
| e2a30d1ac6 | |||
| 3168cbb77d | |||
| 0a0cc0a62c | |||
| adcf0827ee | |||
| b9ee4a152a | |||
| cb839ff149 | |||
| 45efdad27e | |||
| 6ef2bb254d | |||
| 645a8257d9 | |||
| 8ab852277c | |||
| 516ca018d3 | |||
| 5117672388 | |||
| a468ce635c | |||
| 5c58089fb7 | |||
| b3a64892fe | |||
| 3e9c4ba614 | |||
| 8cd17addbe | |||
| 2feaba8888 | |||
| 1909ceed67 | |||
| 07c100bd66 | |||
| ab4776c332 | |||
| f17e0d761e | |||
| a5b9f4c3f1 | |||
| a72f52a5ed | |||
| 6523b906af | |||
| 5d246d449c | |||
| 0b39b2adf6 | |||
| 25a8c1962b | |||
| 9bb7c828cd | |||
| f18dcf9db3 | |||
| 44f8a49b47 | |||
| 10301b8600 | |||
| 32db6b8d44 | |||
| debf015dd0 | |||
| 4013892a47 | |||
| d5277454c6 | |||
| a9f44731dc | |||
| 48808992ec | |||
| 036bc88789 | |||
| 67a7d556ec | |||
| 5ad338e835 | |||
| e442c47eed | |||
| 5380edeeb9 | |||
| e50d1d01da | |||
| 082a803e47 | |||
| 07d9bc0831 | |||
| 4e5a1d4b30 | |||
| 4a54f878c4 | |||
| dc3b4d53e1 | |||
| be583c431e | |||
| 805544ffb0 | |||
| 7b84038bf4 | |||
| e8cbbaa832 | |||
| 56e32e67de | |||
| b36ac532c9 | |||
| d3b0871cf1 | |||
| 7b4204591c | |||
| 4514d72d70 | |||
| dfbf25a9f4 | |||
| 122eac50a6 | |||
| 839708dcfe | |||
| d2066173f0 | |||
| eccad4bbfd | |||
| 98ab794f13 | |||
| b7b2297635 | |||
| dc3f61acee | |||
| 6fffb460b8 | |||
| c677b78f16 | |||
| 014c8af560 | |||
| 2f149eb545 | |||
| 175f0977f9 | |||
| 8fe042218c | |||
| 9fe3718d3f | |||
| 1839f072b4 | |||
| a2cf1b6022 | |||
| 0a4fb4594a | |||
| eb1176eba6 | |||
| a89dfc4524 | |||
| 1ba378dace | |||
| 25c1014ab0 | |||
| 363f553d3c | |||
| 36a6f1ed4b | |||
| 7b112fc448 | |||
| 2a0052dda6 | |||
| a7b32e1330 | |||
| 5adfdc19aa | |||
| 351c86ee7a | |||
| dce7888408 | |||
| a2d99fa6b7 | |||
| 79465571d7 | |||
| 9d576beeb8 | |||
| e3332d1cb6 | |||
| f59f68f894 | |||
| 9c881a02d6 | |||
| 7b21c2d734 | |||
| 9fdc5960bf | |||
| fe853efe32 | |||
| 9b82c03959 | |||
| 914d1b27b5 | |||
| f295d03641 | |||
| 8515f6e6ac | |||
| 4d330e24c1 | |||
| a7a52bc57e | |||
| 3cef7985d3 | |||
| 40db822450 | |||
| 2de202ca02 | |||
| 38eb9fdac7 | |||
| f469d34781 | |||
| 33dfc5ce09 | |||
| 2100e2ff7c | |||
| e9b7cce138 | |||
| 6877a5a15d | |||
| 64206e69bd | |||
| 7643c76cb1 | |||
| b0f59273d3 | |||
| af8eb9d37d | |||
| 635e51f32f | |||
| ca962ce5ad | |||
| a50266cdc0 | |||
| 6230200218 | |||
| f96cd167ef | |||
| 072ce54fe1 | |||
| d043cb9086 | |||
| 1f31df3a94 | |||
| 9ee30e4923 | |||
| 7b44f12ab1 | |||
| 874882b554 | |||
| 945bdf4c60 | |||
| 6e1e5a2afe | |||
| b709b51790 | |||
| d380485bb6 | |||
| 87c8228cd0 | |||
| 152046bf97 | |||
| a0fbed5859 | |||
| 89e9e17d26 | |||
| b595247392 | |||
| 9d50a8cef2 | |||
| f888176485 | |||
| 2f9876ad74 | |||
| 53404122cc | |||
| ba65494fce | |||
| 70645c1732 | |||
| 1055e60d27 | |||
| e04196f8a0 | |||
| 11a0dec047 | |||
| b9740e1b7d | |||
| f0695eb870 | |||
| a40018cdf9 | |||
| 5b7eabe21a | |||
| d5d60aa11b | |||
| a62fa132e6 | |||
| 052395f917 | |||
| 9a77650004 | |||
| f1d70361c9 | |||
| 3496599723 | |||
| 9e0635a6a4 | |||
| 10509621ce | |||
| 3727ecdfe5 | |||
| ac71d22e86 | |||
| bc81356d53 | |||
| 881cb64beb | |||
| 1286e57b63 | |||
| fe5f73d96e | |||
| 8f7a8b31a3 | |||
| 68db35d5d4 | |||
| df17017ced | |||
| 5c48332b0e | |||
| 8985738af5 | |||
| 2d8a676dd5 | |||
| 7e0a9f398c | |||
| 9af5769510 | |||
| bb46d9a009 | |||
| 606b42a6e7 | |||
| d547f5ea22 | |||
| 563b4889e3 | |||
| b449beb68c | |||
| f9d58f4f9c | |||
| 1dfec9902e | |||
| 79cafee2eb | |||
| 64fbcdc1ca | |||
| e4a341af3a | |||
| e0292fe957 | |||
| ef85c8df24 | |||
| 719d369c2a | |||
| 51b6f95342 | |||
| 26fb1fc34d | |||
| ae1578a5e2 | |||
| cfd8e56277 | |||
| 4893931a8d | |||
| 932928ddc8 | |||
| a33e414f01 | |||
| 43d54c8f4f | |||
| 6cbc11a75d | |||
| a21bb130e1 | |||
| 12403785af | |||
| b4892855d4 | |||
| 7ff67f2217 | |||
| 4912c27be8 | |||
| 288ba11452 | |||
| 7874183052 | |||
| b12873f1df | |||
| dc9851f8ea | |||
| ec73170e9b | |||
| 68616e470c | |||
| 53cd2ff524 | |||
| 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 | |||
| 34ef9063cb | |||
| f651d39820 | |||
| 7e6d09a247 | |||
| da381130a3 | |||
| be07cb83c9 | |||
| dfbd86c7bc | |||
| e3ab829ad3 | |||
| b12ef1327c | |||
| d66bcc4b63 | |||
| 5ad307868e | |||
| 369c6ebf85 | |||
| c988d739a1 | |||
| 36ef9f20ae | |||
| c8f118a26b | |||
| be20714842 | |||
| 1711442878 | |||
| 79e6799f40 | |||
| 6023162443 | |||
| 01e0fe4863 | |||
| 1df81e4a34 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1 +1 @@
|
||||
Changelog.md merge=union
|
||||
unreleased.md merge=union
|
||||
|
||||
@ -27,6 +27,9 @@ Issue tracker is ONLY used for reporting bugs with technical details. "It doesn'
|
||||
3.
|
||||
4.
|
||||
|
||||
## Version Information
|
||||
<!--- Which version of the app(s) were you using when you experienced this issue? -->
|
||||
|
||||
## Context (Environment)
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@ -18,23 +18,34 @@ coverage.html
|
||||
mem.pprof
|
||||
|
||||
# Auto generated frontend
|
||||
frontend/qml/BridgeUI/*.qmlc
|
||||
frontend/qml/ProtonUI/*.qmlc
|
||||
frontend/qml/ProtonUI/fontawesome.ttf
|
||||
frontend/qml/ProtonUI/images
|
||||
internal/frontend/qml/BridgeUI/*.qmlc
|
||||
internal/frontend/qml/ImportExportUI/*.qmlc
|
||||
internal/frontend/qml/ProtonUI/*.qmlc
|
||||
internal/frontend/qml/ProtonUI/fontawesome.ttf
|
||||
internal/frontend/qml/ProtonUI/images
|
||||
internal/frontend/qml/ImportExportUI/images
|
||||
frontend/qml/*.qmlc
|
||||
|
||||
# Build files
|
||||
bridge_darwin_*.tgz
|
||||
/launcher-*
|
||||
/bridge_*_*.tgz
|
||||
/ie_*_*.tgz
|
||||
/versioner
|
||||
/hasher
|
||||
cmd/Desktop-Bridge/deploy
|
||||
internal/frontend/qt/moc.cpp
|
||||
internal/frontend/qt/moc.go
|
||||
internal/frontend/qt/moc.h
|
||||
internal/frontend/qt/moc_cgo_darwin_darwin_amd64.go
|
||||
internal/frontend/qt/moc_moc.h
|
||||
internal/frontend/qt/rcc.cpp
|
||||
internal/frontend/qt/rcc_cgo_darwin_darwin_amd64.go
|
||||
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_darwin_darwin_amd64.go
|
||||
internal/frontend/rcc_cgo_*.go
|
||||
vendor-cache/
|
||||
|
||||
/main.go
|
||||
|
||||
190
.gitlab-ci.yml
190
.gitlab-ci.yml
@ -1,4 +1,4 @@
|
||||
image: gitlab.protontech.ch:4567/go/bridge/ci
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:latest
|
||||
|
||||
before_script:
|
||||
- eval $(ssh-agent -s)
|
||||
@ -13,38 +13,15 @@ before_script:
|
||||
cache:
|
||||
key: go-mod
|
||||
paths:
|
||||
- .cache
|
||||
- .cache
|
||||
policy: pull
|
||||
|
||||
stages:
|
||||
- image
|
||||
- cache
|
||||
- test
|
||||
- build
|
||||
- mirror
|
||||
|
||||
# Stage: IMAGE
|
||||
|
||||
build-ci-image:
|
||||
stage: image
|
||||
image: docker:stable
|
||||
before_script: []
|
||||
cache: {}
|
||||
tags:
|
||||
- heavy
|
||||
only:
|
||||
changes:
|
||||
- ci/*
|
||||
services:
|
||||
- docker:dind
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker info
|
||||
- docker build -t gitlab.protontech.ch:4567/go/bridge/ci:latest ci
|
||||
- docker push gitlab.protontech.ch:4567/go/bridge/ci:latest
|
||||
|
||||
# Stage: CACHE
|
||||
|
||||
# This will ensure latest dependency versions and updates the cache for
|
||||
@ -54,11 +31,11 @@ cache-push:
|
||||
only:
|
||||
- branches
|
||||
script:
|
||||
- echo ""
|
||||
- echo ""
|
||||
cache:
|
||||
key: go-mod
|
||||
paths:
|
||||
- .cache
|
||||
- .cache
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
@ -67,7 +44,9 @@ lint:
|
||||
only:
|
||||
- branches
|
||||
script:
|
||||
- make lint
|
||||
- env GOMAXPROCS=$(( ${CI_TAG_CPU} / 2 )) make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test:
|
||||
stage: test
|
||||
@ -83,6 +62,8 @@ test:
|
||||
- pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'`
|
||||
# Then finally run the tests
|
||||
- make test
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-integration:
|
||||
stage: test
|
||||
@ -90,6 +71,8 @@ test-integration:
|
||||
- branches
|
||||
script:
|
||||
- VERBOSITY=debug make -C test test
|
||||
tags:
|
||||
- large
|
||||
|
||||
dependency-updates:
|
||||
stage: test
|
||||
@ -98,23 +81,59 @@ dependency-updates:
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
build-linux:
|
||||
.build-base:
|
||||
stage: build
|
||||
# Test build every time (= we want to know build is possible).
|
||||
only:
|
||||
- branches
|
||||
script:
|
||||
- make build
|
||||
artifacts:
|
||||
# Note: The latest artifacts for refs are locked against deletion, and kept regardless of the expiry time.
|
||||
# Introduced in GitLab 13.0 behind a disabled feature flag, and made the default behavior in GitLab 13.4.
|
||||
expire_in: 1 day
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends: .build-base
|
||||
artifacts:
|
||||
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
expire_in: 2 week
|
||||
|
||||
build-darwin:
|
||||
stage: build
|
||||
build-linux-qa:
|
||||
extends: .build-base
|
||||
only:
|
||||
- branches
|
||||
- web
|
||||
script:
|
||||
- BUILD_TAGS="build_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-ie-linux-qa:
|
||||
extends: .build-base
|
||||
only:
|
||||
- web
|
||||
script:
|
||||
- BUILD_TAGS="build_qa" make build-ie
|
||||
artifacts:
|
||||
name: "ie-linux-qa-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- ie_*.tgz
|
||||
|
||||
.build-darwin-base:
|
||||
extends: .build-base
|
||||
before_script:
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
|
||||
@ -125,25 +144,58 @@ build-darwin:
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOPATH=~/go
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS-bridge
|
||||
script:
|
||||
- make build
|
||||
- macOS
|
||||
|
||||
build-darwin:
|
||||
extends: .build-darwin-base
|
||||
artifacts:
|
||||
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
expire_in: 2 week
|
||||
|
||||
build-windows:
|
||||
stage: build
|
||||
services:
|
||||
- docker:dind
|
||||
build-darwin-qa:
|
||||
extends: .build-darwin-base
|
||||
only:
|
||||
- branches
|
||||
- web
|
||||
script:
|
||||
- BUILD_TAGS="build_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-ie-darwin-qa:
|
||||
extends: .build-darwin-base
|
||||
only:
|
||||
- web
|
||||
script:
|
||||
- BUILD_TAGS="build_qa" make build-ie
|
||||
artifacts:
|
||||
name: "ie-darwin-qa-$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.
|
||||
@ -156,7 +208,57 @@ build-windows:
|
||||
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
expire_in: 2 week
|
||||
|
||||
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" 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
|
||||
|
||||
build-ie-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" make build-ie
|
||||
artifacts:
|
||||
name: "ie-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- ie_*.tgz
|
||||
|
||||
# Stage: MIRROR
|
||||
|
||||
mirror-repo:
|
||||
stage: mirror
|
||||
|
||||
@ -21,6 +21,12 @@ issues:
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
|
||||
linters-settings:
|
||||
godox:
|
||||
keywords:
|
||||
- TODO
|
||||
- FIXME
|
||||
|
||||
linters:
|
||||
# setting disable-all will make only explicitly enabled linters run
|
||||
disable-all: true
|
||||
|
||||
56
BUILDS.md
56
BUILDS.md
@ -1,6 +1,7 @@
|
||||
# Building ProtonMail Bridge app
|
||||
# Building ProtonMail Bridge and Import-Export app
|
||||
|
||||
## Prerequisites
|
||||
* 64-bit OS (the go-rfc5322 module cannot currently be compiled for 32-bit OSes)
|
||||
* Go 1.13
|
||||
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
||||
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||
@ -13,12 +14,22 @@ To enable the sending of crash reports using Sentry please set the
|
||||
Otherwise, the sending of crash reports will be disabled.
|
||||
|
||||
## Build
|
||||
* for Windows please unset the `MSYSTEM` variable
|
||||
In order to build Bridge or Import-Export app with Qt interface we are using
|
||||
[Qt Go Binding](https://github.com/therecipe/qt). The dependencies and
|
||||
installation of this tool is part of `make build` target. If you have issues
|
||||
with installation of therecipe/qt we recommend to follow [this
|
||||
wiki](https://github.com/therecipe/qt/wiki/Installation-on-Linux)
|
||||
|
||||
Please note that `$(go env GOPATH)/bin` must be in your `PATH` to ensure
|
||||
binaries installed by `therecipe/qt` (such as `qtdeploy`) are found. Also,
|
||||
before you start build **on Windows**, please unset the `MSYSTEM` variable
|
||||
|
||||
|
||||
```bash
|
||||
export MSYSTEM=
|
||||
```
|
||||
|
||||
### Build Bridge
|
||||
* in project root run
|
||||
|
||||
```bash
|
||||
@ -26,9 +37,44 @@ make build
|
||||
```
|
||||
|
||||
* The result will be stored in `./cmd/Destop-Bridge/deploy/${GOOS}/`
|
||||
* for `linux`, the binary will have the name of the project directory (e.g `bridge`)
|
||||
* for `windows`, the binary will have the file extension `.exe` (e.g `bridge.exe`)
|
||||
* for `darwin`, the application will be created with name of the project directory (e.g `bridge.app`)
|
||||
* for `linux`, the binary will have the name of the project directory (e.g `proton-bridge`)
|
||||
* for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`)
|
||||
* for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`)
|
||||
|
||||
#### Build Bridge without GUI
|
||||
* If you need to build bridge without Qt dependencies, you can do so by running
|
||||
|
||||
```bash
|
||||
make build-nogui
|
||||
```
|
||||
|
||||
* Bridge without GUI will start by default without any interface (i.e., there is no way to add or remove client, get bridge password, etc)
|
||||
* Bridge always has the option (whether built with Qt or without) to use a CLI interface by starting it with the argument `-c`
|
||||
* NOTE: You still need to setup supported keychain on your system
|
||||
|
||||
### 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`)
|
||||
|
||||
### Launchers
|
||||
Launchers are only included in official distributions and provide the public
|
||||
key used to verify signed app binaries, allowing the automatic update feature.
|
||||
See README for more information.
|
||||
|
||||
### Tags
|
||||
Note that repository contains both Bridge and Import-Export apps and they are
|
||||
not released together. Therefore, each app has own tag prefix. Bridge tags
|
||||
starts with `br-` and Import-Export tags starts with `ie-`. Both tags continue
|
||||
with semantic versioning `MAJOR.MINOR.PATCH`. An example of full tag is
|
||||
`br-1.4.4` or `ie-1.1.2` (current versions in October 2020).
|
||||
|
||||
## Useful tests, lints and checks
|
||||
In order to be able to run following commands please install the development dependencies:
|
||||
|
||||
@ -41,7 +41,6 @@ ProtonMail Bridge includes the following 3rd party software:
|
||||
* [imap-appendlimit](https://github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/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](https://github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/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](https://github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
||||
* [smtp](https://github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
||||
* [textwrapper](https://github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||
451
Changelog.md
451
Changelog.md
@ -1,10 +1,375 @@
|
||||
# ProtonMail Bridge Changelog
|
||||
# ProtonMail Bridge and Import-Export app Changelog
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## Unreleased
|
||||
## [Bridge 1.6.3] HZM
|
||||
|
||||
## [v1.3.x] Emma (beta since 2020-08-05)
|
||||
### Added
|
||||
* GODT-337 Desktop files.
|
||||
|
||||
### Changed
|
||||
* GODT-885 Do not explicitly unlabel folders during move to match behaviour of other clients.
|
||||
* GODT-616 Better user message about wrong mailbox password.
|
||||
* GODT-1021 Do not allow copy Inbox->Sent or Sent->Inbox
|
||||
* GODT-976 Exclude updates from clearing cache and clear cache, including updates, while switching early access off
|
||||
* GODT-1033 Retry starting IMAP server after connection was down
|
||||
|
||||
### Fixed
|
||||
* GODT-1011 Stable integration test deleting many messages using UID EXPUNGE.
|
||||
* GODT-1015 Use lenient version parser to properly parse version provided by Mac.
|
||||
* GODT-919 Notify about update right after the start.
|
||||
* GODT-919 GODT-1022 Logs and signals
|
||||
|
||||
|
||||
## [IE 1.3.0] Farg
|
||||
|
||||
### Changed
|
||||
* GODT-1019 Remove dependency on go-apple-mobileconfig.
|
||||
* GODT-928 Reject messages which are too large.
|
||||
* GODT-999 Sending: do not send empty objects to API.
|
||||
|
||||
## [Bridge 1.6.2] HZM
|
||||
|
||||
### Fixed
|
||||
* GODT-1010 Strip angle brackets from external ID.
|
||||
|
||||
## [Bridge 1.6.1] HZM
|
||||
|
||||
### Added
|
||||
* GODT-1007 Notify user when version is the latest.
|
||||
|
||||
### Fixed
|
||||
* GODT-787 GODT-978 Fix IE and Bridge importing to Sent not showing up in Inbox (setting up flags properly).
|
||||
* GODT-1006 Use correct macOS keychain name.
|
||||
* GODT-1009 Set ContentID if present and not explicitly attachment.
|
||||
* GODT-1008 Transparent welcome message.
|
||||
|
||||
|
||||
## [Bridge 1.6.0] HZM
|
||||
|
||||
### Added
|
||||
* GODT-705 Allow silent update in Bridge and Import-Export app.
|
||||
* GODT-958 Release notes per eaach update channel.
|
||||
* GODT-875 Added GUI dialog on force update.
|
||||
* GODT-820 Added GUI notification on impossibility of update installation (both silent and manual).
|
||||
* GODT-870 Added GUI notification on error during silent update.
|
||||
* GODT-805 Added GUI notification on update available.
|
||||
* GODT-804 Added GUI notification on silent update installed (promt to restart).
|
||||
* GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled).
|
||||
* GODT-874 Added manual triggers to Updater module.
|
||||
* GODT-851 Added support of UID EXPUNGE.
|
||||
|
||||
### Removed
|
||||
* GODT-248 Remove dependency on go-appdir.
|
||||
* GODT-208 Remove deprecated use of BuildNameToCertificate.
|
||||
|
||||
### Fixed
|
||||
* Check deprecated status code first to better determine API error.
|
||||
* GODT-831 Fix reporting bug from accounts with empty account name.
|
||||
* GODT-831 Cancel request of uploading attachment if reading/writing it fails.
|
||||
* GODT-991 Fix panic when stopping import progress during loading mailboxes info.
|
||||
* GODT-895 Fix panic when modifying addresses during changing address mode.
|
||||
* GODT-946 Fix flaky tests notifying changes.
|
||||
* GODT-979 Fix panic when trying to parse a multipart/alternative section that has no child sections.
|
||||
* GODT-900 Remove \Deleted flag after re-importing the message (do not delete messages by moving to local folder and back).
|
||||
|
||||
### Changed
|
||||
* Rename channels `beta->early`, `live->stable`.
|
||||
* Bump gopenpgp dependency to v2.1.3 for improved memory usage.
|
||||
* GODT-97 Don't log errors caused by SELECT "".
|
||||
* GODT-806 GUI dialog on manual update. Added autoupdates checkbox. Simplifyed installation process GUI.
|
||||
* GODT-912 Scroll bar behaviour in settings tab.
|
||||
* GODT-149 Send heartbeat ASAP on each new calendar day.
|
||||
* GODT-792 Stop IMAP server while no internet connection.
|
||||
* GODT-792 Cache message size every time to reduce network traffic.
|
||||
* GODT-792 Cache body structure in order to reduce network traffic.
|
||||
* GODT-792 GODT-908 Cache body structure in order to reduce network traffic.
|
||||
* GODT-908 Do not unpause event loop if other mailbox is still fetching.
|
||||
|
||||
|
||||
## [Bridge 1.5.7] Golden Gate
|
||||
|
||||
### Fixed
|
||||
CSB-331 Fix sending error due to mixed case in sender address.
|
||||
|
||||
## [Bridge 1.5.6] Golden Gate
|
||||
|
||||
### Added
|
||||
* GODT-797 EXPUNGE waits for APPEND to prevent data loss when Outlook moves from Spam to Inbox.
|
||||
|
||||
|
||||
## [Bridge 1.5.5] Golden Gate
|
||||
|
||||
### Changed
|
||||
* GODT-922 Fix panic during restarting the bridge.
|
||||
* GODT-945 Fix panic in integration tests caused by concurrent map writes.
|
||||
* GODT-732 Fix usage of fontawesome.
|
||||
* GODT-951 Properly parse message with long lines in header and long header split to multiple lines (upgrading to latest go-message).
|
||||
* GODT-894 Fix panic when sending while account is logging in.
|
||||
* GODT-858 Bump go-rfc5322 dependency to v0.5.0 to handle some invalid RFC5322 groups and add support for semicolon delimiter in address-list.
|
||||
* GODT-923 Fix listener locking.
|
||||
* GODT-389 Prefer `From` header instead of `MAIL FROM` address.
|
||||
* GODT-898 Only set ContentID for inline attachments.
|
||||
* GODT-773 Replace `INTERNALDATE` older than birthday of RFC822 by birthday of RFC822 to not crash Apple Mail.
|
||||
* GODT-927 Avoid to call API with empty label name.
|
||||
* GODT-915 Bump go-imap dependency and remove go-imap-specialuse dependency.
|
||||
|
||||
## [Bridge 1.5.4] Golden Gate
|
||||
|
||||
### Added
|
||||
* Log warning about permanently deleting messages.
|
||||
|
||||
### Fixed
|
||||
* License path on Arch and Windows.
|
||||
|
||||
## [Bridge 1.5.3] Golden Gate [Import-Export 1.2.3] Elbe
|
||||
|
||||
### Added
|
||||
* GODT-906 Handle RFC2047-encoded content transfer encoding values.
|
||||
* GODT-887 Make supports build with native Qt.
|
||||
|
||||
### Changed
|
||||
* GODT-893 Bump go-rfc5322 dependency to v0.2.1 to properly detect syntax errors during parsing.
|
||||
* GODT-892 Swap type and value from sentry exception and cut panic handlers from the traceback.
|
||||
* GODT-854 EXPUNGE and FETCH unilateral responses are returned before OK EXPUNGE or OK STORE, respectively.
|
||||
* #109 Renamed COPYING.md to not be read by [pkg-go-dev](https://pkg.go.dev/license-policy).
|
||||
|
||||
### Removed
|
||||
* GODT-651 Build creates proper binary names.
|
||||
* GODT-148 Allow import (using the Import-Export app) of already encrypted messages as is.
|
||||
* GODT-202 Update to latest go-smtp.
|
||||
|
||||
### Fixed
|
||||
* GODT-135 Support parameters in SMTP `FROM MAIL` command, such as `BODY=7BIT`, or empty value `FROM MAIL:<>` used by some clients.
|
||||
* GODT-338 GODT-781 GODT-857 GODT-866 Flaky tests.
|
||||
* GODT-773 Replace old dates with birthday of RFC822 to not crash Apple Mail. Original is available under `X-Original-Date` header.
|
||||
|
||||
## [Bridge 1.5.2] Golden Gate
|
||||
|
||||
### Changed
|
||||
* GODT-883 Use `ClearPacket` for `text/plain` with signature.
|
||||
|
||||
|
||||
## [Bridge 1.5.1] Golden Gate
|
||||
|
||||
### Added
|
||||
* GODT-701 Try load messages one-by-one if IMAP server errors with batch load
|
||||
and not interrupt the transfer.
|
||||
* GODT-878 Tests for send packet creation logic.
|
||||
|
||||
### Changed
|
||||
* GODT-651 Build creates proper binary names.
|
||||
* GODT-878 Fix an issue where the random session key is inadvertently sent to
|
||||
the Proton server. The data payload is always encrypted within TLS, but this
|
||||
is still a potential privacy problem. Discovered by Proton's internal
|
||||
security audit team.
|
||||
* GODT-878 Refactor and move the send packet creation logic to `pmapi.SendMessageReq`.
|
||||
* GODT-878 Encryption of session keys moved to pmapi.
|
||||
|
||||
|
||||
## [IE 1.2.1, 1.2.2] Elbe
|
||||
|
||||
### Added
|
||||
* GODT-799 Skipped messages do not change total counts but shows as separate number.
|
||||
|
||||
### Fixed
|
||||
* GODT-799 Fix skipping unwanted folders importing from mbox files.
|
||||
* GODT-769 Close connection before deleting labels to prevent panics accessing deleted bucket.
|
||||
|
||||
### Removed
|
||||
* GODT-766 Remove GUI popup for IMAP TLS error.
|
||||
|
||||
|
||||
## [Bridge 1.5.0] Golden Gate
|
||||
|
||||
### Changed
|
||||
* Updated go-mbox dependency back to upstream.
|
||||
|
||||
### Fixed
|
||||
* GODT-847 Waiting for unilateral update during deleting the message.
|
||||
* GODT-849 Show in error counts in the end also lost messages.
|
||||
* GODT-835 Do not include conversation ID in references to show properly conversation threads in clients.
|
||||
* GODT-685 Improve deb packaging regarding dejavu font.
|
||||
|
||||
|
||||
## [IE 1.2.0] Elbe
|
||||
|
||||
### Added
|
||||
* GODT-763 Detect Gmail labels from All Mail mbox export (using X-Gmail-Label header).
|
||||
* GODT-834 Info about tags in BUILDS.md and link to Import-Export page in README.md.
|
||||
* GODT-777 Support Apple Mail MBOX export format.
|
||||
* GODT-731 Re-open Import-Export app from the second instance.
|
||||
|
||||
### Fixed
|
||||
* GODT-677 Windows IE: global import settings not fit in window.
|
||||
* GODT-794 Congo fails to update to Danube.
|
||||
* GODT-749 Don't force PGP/Inline when sending plaintext messages.
|
||||
* GODT-764 Fix deadlock in integration tests for Import-Export.
|
||||
* GODT-662 Do not resume paused transfer progress after dismissing cancel popup.
|
||||
* GODT-772 Sanitize mailbox names for exporting to follow OS restrictions.
|
||||
* GODT-771 Show fatal errors after export is terminated.
|
||||
* GODT-779 Do not propagate updates when progress is stopped.
|
||||
* GODT-779 Unpause progress during fatal error to properly stop progress.
|
||||
* GODT-779 Stop ongoing transfer calls sooner (re-check after import request is generated).
|
||||
* Fix measurement of uploading attachments during transfer.
|
||||
* GODT-827 Do not spam sentry with bad ID by integration test.
|
||||
* GODT-700 Fix UTF-7 incompatibility.
|
||||
* GODT-837 Fix flaky TestFailUnpauseAndStops.
|
||||
* GODT-782 Don't use TLS pinning when checking connectivity status.
|
||||
|
||||
### Changed
|
||||
* TLS pins conform to official list.
|
||||
|
||||
|
||||
## [Bridge 1.4.5] Forth
|
||||
|
||||
### Fixed
|
||||
* GODT-829 Remove `NoInferior` to display sub-folders in apple mail.
|
||||
|
||||
## [Bridge 1.4.4] Forth
|
||||
|
||||
### Fixed
|
||||
* GODT-798 Replace, don't add, transfer encoding when making body 7-bit clean.
|
||||
* Move/Copy duplicate for emails with References in Outlook.
|
||||
* CSB-247 Cannot update from 1.4.0.
|
||||
|
||||
|
||||
## [Bridge 1.4.3] Forth
|
||||
|
||||
### Changed
|
||||
* Reverted sending IMAP updates to be not blocking again.
|
||||
|
||||
### Fixed
|
||||
* GODT-783 Settings flags by FLAGS (not using +/-FLAGS) do not change spam state.
|
||||
|
||||
|
||||
## [Bridge 1.4.2] Forth
|
||||
|
||||
### Changed
|
||||
* GODT-761 Use label.Path instead of Name to partially support subfolders for webapp beta release.
|
||||
* GODT-765 Improve speed of checking whether message is deleted.
|
||||
|
||||
|
||||
## [IE 1.1.2] Danube (beta 2020-09-xx)
|
||||
|
||||
### Fixed
|
||||
* GODT-770 Better handling of extraneous end-of-mail indicator.
|
||||
* GODT-776 Fix crash when IMAP client connects while account is logging in.
|
||||
* GODT-744 User agent not being sent to sentry.
|
||||
|
||||
### Changed
|
||||
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8.
|
||||
* GODT-785 Clear separation of different message IDs in integration tests.
|
||||
### Changed
|
||||
* GODT-741 Import-Export shows "Unable to parse time" notice instead of zero time in error report window.
|
||||
|
||||
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8.
|
||||
* GODT-374 Allow to send calendar update multiple times.
|
||||
|
||||
## [IE 1.1.1] Danube (beta 2020-09-xx) [Bridge 1.4.1] Forth (beta 2020-09-xx)
|
||||
|
||||
### Fixed
|
||||
* GODT-752 Parsing message with empty addresses.
|
||||
* GODT-752 Parsing non-utf8 multipart/alternative message.
|
||||
* GODT-752 Parsing message with duplicate charset parameter.
|
||||
|
||||
|
||||
## [IE 1.1.0] Danube
|
||||
|
||||
### Fixed
|
||||
* GODT-703 Import-Export showed always at least one total message.
|
||||
* GODT-738 Fix for mbox files with long lines.
|
||||
### Fixed
|
||||
* GODT-732 Do not mix font awesome icon with regular text to avoid issues on Fedora.
|
||||
|
||||
|
||||
## [Bridge 1.4.0] Forth
|
||||
|
||||
### Added
|
||||
* GODT-682 Persistent anonymous API cookies for Import-Export.
|
||||
* GODT-357 Use go-message to make a better message parser.
|
||||
* GODT-720 Time measurement of progress for Import-Export.
|
||||
* GODT-693 Launcher.
|
||||
|
||||
### Changed
|
||||
* GODT-511 User agent format changed.
|
||||
* Unsilent errors reading mbox files.
|
||||
* GODT-692 QA build with option to change API URL by ENV variable.
|
||||
* GODT-704 User agent detected by fake IMAP extension instead of AUTH callback (some clients use LOGIN instead of AUTH).
|
||||
* GODT-695 Parallel upload for ProtonMail target.
|
||||
|
||||
### Removed
|
||||
* GODT-519 Unused AUTH scope parsing methods.
|
||||
|
||||
### Fixed
|
||||
* GODT-698 Use correct package type for signed PGP/Inline messages.
|
||||
* Generic bug report window title.
|
||||
* Fix missing check for unencrypted recipients during sending.
|
||||
* Version checking for catalina.
|
||||
* GODT-730 Limit maximal TLS version for Yahoo IMAP server.
|
||||
|
||||
|
||||
## [IE 1.0.x] Congo (v1.0.0 live 2020-09-08)
|
||||
|
||||
### Added
|
||||
* GODT-633 Persistent anonymous API cookies for better load balancing and abuse detection.
|
||||
* GODT-461 Add support for `\Deleted` flag.
|
||||
|
||||
### Changed
|
||||
* GODT-462 Pausing event loop while FETCHing to prevent EXPUNGE.
|
||||
* Wait for unilateral response to be delivered.
|
||||
* GODT-409 Set flags have to replace all flags.
|
||||
* GODT-531 Better way to add trusted certificate in macOS.
|
||||
* Bumped golangci-lint to v1.29.0.
|
||||
* GODT-549 Check log file size more often to prevent huge log files.
|
||||
* Bumped various dependencies:
|
||||
* Updated andybalholm/cascadia v1.1.0 -> v1.2.0.
|
||||
* Updated emersion/go-imap-specialuse 20161227184202-ba031ced6a62 -> 20200722111535-598ff00e4075.
|
||||
* Updated emersion/go-sasl 20191210011802-430746ea8b9b -> 20200509203442-7bfe0ed36a21.
|
||||
* Updated github.com/go-resty/resty/v2 v2.2.0 -> v2.3.0.
|
||||
* Updated github.com/golang/mock v1.4.3 -> v1.4.4.
|
||||
* Updated github.com/google/go-cmp v0.4.0 -> v0.5.1.
|
||||
* Updated github.com/hashicorp/go-multierror v1.0.0 -> v1.1.0.
|
||||
* Updated github.com/jaytaylor/html2text 20200220170450-61d9dc4d7195 -> 20200412013138-3577fbdbcff7.
|
||||
* Updated github.com/jhillyerd/enmime v0.8.0 -> v0.8.1.
|
||||
* Updated github.com/keybase/go-keychain 20200218013740-86d4642e4ce2 -> 20200502122510-cda31fe0c86d.
|
||||
* Updated github.com/logrusorgru/aurora 20200102142835-e9ef32dff381 -> v2.0.3+incompatible.
|
||||
* Updated github.com/miekg/dns v1.1.29 -> v1.1.30.
|
||||
* Updated github.com/nsf/jsondiff 20190712045011-8443391ee9b6 -> 20200515183724-f29ed568f4ce.
|
||||
* Updated github.com/sirupsen/logrus v1.4.2 -> v1.6.0.
|
||||
* Updated github.com/stretchr/testify v1.5.1 -> v1.6.1.
|
||||
* Updated github.com/therecipe/qt 20200126204426-5074eb6d8c41 -> 20200701200531-7f61353ee73e.
|
||||
* Updated github.com/urfave/cli v1.22.3 -> v1.22.4.
|
||||
* Updated golang.org/x/net 20200301022130-244492dfa37a -> 20200707034311-ab3426394381.
|
||||
* Updated golang.org/x/text v0.3.2 -> v0.3.3.
|
||||
* Set first-start to false in bridge, not in frontend.
|
||||
* GODT-400 Refactor sendingInfo.
|
||||
* GODT-513 Update routes to API v4.
|
||||
* GODT-551 Do not ignore errors during message flagging.
|
||||
* GODT-380 Adding IE GUI to Bridge repo and building.
|
||||
* BR: extend functionality of PopupDialog.
|
||||
* BR: makefile APP_VERSION instead of BRIDGE_VERSION.
|
||||
* BR: use common logs function for Qt.
|
||||
* BR: change `go.progressDescription` to `string`.
|
||||
* IE: Rounded button has fa-icon.
|
||||
* IE: `Upgrade` → `Update`.
|
||||
* IE: Moving `AccountModel` to `qt-common`.
|
||||
* IE: Added `ReportBug` to `internal/importexport`.
|
||||
* IE: Added event watch in GUI.
|
||||
* IE: Removed `onLoginFinished`.
|
||||
* Structure for transfer rules in QML.
|
||||
* GODT-213 Convert panics from message parser to error.
|
||||
* GODT-585 Do not allow deleting messages from All Mail.
|
||||
|
||||
### Fixed
|
||||
* GODT-655 Fix date picker with automatic Windows DST.
|
||||
* GODT-454 Fix send on closed channel when receiving unencrypted send confirmation from GUI.
|
||||
* GODT-597 Duplicate sending when draft creation takes too long.
|
||||
* GODT-634 Hover on links in popups.
|
||||
|
||||
|
||||
|
||||
## [Bridge 1.3.x] Emma (v1.3.2 beta 2020-08-04, v1.3.3 beta 2020-08-06, v1.3.3 live 2020-08-12)
|
||||
|
||||
### Added
|
||||
* GODT-554 Detect and notify about "bad certificate" IMAP TLS error.
|
||||
@ -64,7 +429,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Issue causing deadlock when reloading users keys due to double-locking of a mutex.
|
||||
* Correctly handle failure to unlock single key.
|
||||
* GODT-479 Fix flaky integration tests.
|
||||
* GODT-484 Fix infinite loop when decoding invalid 2231 charset
|
||||
* GODT-484 Fix infinite loop when decoding invalid 2231 charset.
|
||||
* GODT-267 Correctly detect if a message is a draft even if does not have DraftLabel.
|
||||
* GODT-308 Reduce minimum read speed threshold to avoid issues with flaky internet.
|
||||
* GODT-321 Changing address ordering would cause all messages to disappear in combined mode.
|
||||
@ -73,7 +438,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-427 Fix race condition in auth refresh that could cause user to be logged out.
|
||||
|
||||
|
||||
## [v1.2.8] Donghai-fix-append (beta 2020-06-XXX)
|
||||
## [Bridge 1.2.8] Donghai-fix-append (beta 2020-06-XXX)
|
||||
|
||||
### Changed
|
||||
* GODT-396 reduce number of EXISTS calls.
|
||||
@ -82,7 +447,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
### Fixed
|
||||
* GODT-502 Fixed crash when unable to parse a message header.
|
||||
|
||||
## [v1.2.7] Donghai-fix-sync - (beta 2020-05-07 live 2020-04-20)
|
||||
## [Bridge 1.2.7] Donghai-fix-sync - (beta 2020-05-07 live 2020-04-20)
|
||||
|
||||
### Added
|
||||
* IMAP extension MOVE with UIDPLUS support.
|
||||
@ -105,7 +470,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Use correct binary name when finding location of addcert.scpt.
|
||||
|
||||
|
||||
## [v1.2.6] Donghai - beta (2020-03-31)
|
||||
## [Bridge 1.2.6] Donghai - beta (2020-03-31)
|
||||
|
||||
### Added
|
||||
* GODT-145 Support drafts.
|
||||
@ -160,13 +525,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* UserIDs were not checked when importing to Sent folder (affects copying from account1/sent to account2/sent).
|
||||
|
||||
|
||||
## [v1.2.5] Charles - live (2020-03-11) beta (from 2020-02-10)
|
||||
|
||||
### Hotfix
|
||||
* CSB-40 panic in credential store.
|
||||
* Keyring unlocking locker.
|
||||
* No panic on failed html parse.
|
||||
* Too many open files.
|
||||
## [Bridge 1.2.5] Charles - live (2020-03-11) beta (from 2020-02-10)
|
||||
|
||||
### Added
|
||||
* GODT-112 Migration of preferences from c10 to c11.
|
||||
@ -211,13 +570,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Separated IMAP to store and IMAP.
|
||||
* Store is responsible for everything about db and calls to pmapi, including event loop, sync, address mode.
|
||||
* IMAP is responsible only for IMAP interfaces.
|
||||
* Event loop is only one per ProtonMail account (instead of one per alias)
|
||||
* It also means only one database per account (instead of one per address)
|
||||
* Changing address mode is not destroying database, only buckets with IDs mapping (keeping metadata for account)
|
||||
* Event loop is only one per ProtonMail account (instead of one per alias).
|
||||
* It also means only one database per account (instead of one per address).
|
||||
* Changing address mode is not destroying database, only buckets with IDs mapping (keeping metadata for account).
|
||||
* Before first sync we set event ID so we will not miss changes happening during sync.
|
||||
* Thanks to previous point we are not starting new sync when we finish first one because of unprocessed events.
|
||||
* Sync is not blocking event loop (user can get new messages even during sync)
|
||||
* Sync is not blocking reading operations (user can list mailboxes even before first sync is done)
|
||||
* Sync is not blocking event loop (user can get new messages even during sync).
|
||||
* Sync is not blocking reading operations (user can list mailboxes even before first sync is done).
|
||||
* Sync is not blocking writing operations such as mark messages read/unread and so on.
|
||||
* Most operations have to be passed to API and only event loop is writing them to the database.
|
||||
* Avoid relying on counts API endpoint; use event counts as much as possible.
|
||||
@ -227,8 +586,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Synchronisation will create a label if not yet present.
|
||||
* Labels and Folders (including system folders) are stored in DB together with their counts for offline read-out.
|
||||
* AddressIDs for all user addresses are stored in DB.
|
||||
* IMAP updates channel is set when an IMAP client connects (and IMAP updates are dropped until then)
|
||||
* DB keeps track of address mode (split/combined)
|
||||
* IMAP updates channel is set when an IMAP client connects (and IMAP updates are dropped until then).
|
||||
* DB keeps track of address mode (split/combined).
|
||||
* Event loop starts as soon as user is initialised (i.e. logged in), not just when imap is connected.
|
||||
* Use pmapi v1.0.13.
|
||||
* Logout user if initialisation fails.
|
||||
@ -236,6 +595,10 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Use godog v0.8.0 under new name 'cucumber' (instead of DATA-DOG).
|
||||
|
||||
### Fixed
|
||||
* CSB-40 panic in credential store.
|
||||
* Keyring unlocking locker.
|
||||
* No panic on failed html parse.
|
||||
* Too many open files.
|
||||
* #1057 Logging in to an already logged in user would display unrelated error "invalid mailbox password".
|
||||
* #1056 Changing mailbox password sometimes didn't log out user.
|
||||
* #1066 Split address mode can not work when credentials store is cleared.
|
||||
@ -251,7 +614,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-103 User keys were not unlocked later if they were not unlocked during startup.
|
||||
|
||||
|
||||
## [v1.2.4] Brooklyn beta (2019-12-16)
|
||||
## [Bridge 1.2.4] Brooklyn beta (2019-12-16)
|
||||
|
||||
### Added
|
||||
* #976: fix slow authentication.
|
||||
@ -266,7 +629,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Fixed an issue where entering an in-use port multiple times via the CLI would make bridge use it.
|
||||
* Update therecipe/qt and Qt to 5.13.
|
||||
|
||||
## [v1.2.3] Akashi - live (2019-11-05) beta (2019-10-22)
|
||||
## [Bridge 1.2.3] Akashi - live (2019-11-05) beta (2019-10-22)
|
||||
|
||||
### Added
|
||||
* #963 report first-start metric with bridge version.
|
||||
@ -296,17 +659,17 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Code made compatible with name changes in go-pmapi.
|
||||
|
||||
|
||||
## [v1.2.2] - beta and live 2019-09-06
|
||||
## [Bridge 1.2.2] - beta and live 2019-09-06
|
||||
|
||||
### Changed
|
||||
* User compare case insensitive.
|
||||
|
||||
## [v1.2.1] - beta and live 2019-09-05
|
||||
## [Bridge 1.2.1] - beta and live 2019-09-05
|
||||
|
||||
### Changed
|
||||
* #924 fix start of bridge without internet connection.
|
||||
|
||||
## [v1.2.0] - beta 2019-08-22
|
||||
## [Bridge 1.2.0] - beta 2019-08-22
|
||||
|
||||
### Added
|
||||
* #903 added http.Client timeout to not hang out forever.
|
||||
@ -406,7 +769,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Handle logout in event loop.
|
||||
|
||||
|
||||
## [v1.1.6] - 2019-07-09 (beta 2019-07-01)
|
||||
## [Bridge 1.1.6] - 2019-07-09 (beta 2019-07-01)
|
||||
|
||||
### Added
|
||||
* #841 assume text/plain during sending e-mails when missing content type.
|
||||
@ -438,7 +801,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Lint corrections.
|
||||
|
||||
|
||||
## [v1.1.5] - 2019-05-23 (beta 2019-05-23, 2019-05-16)
|
||||
## [Bridge 1.1.5] - 2019-05-23 (beta 2019-05-23, 2019-05-16)
|
||||
|
||||
### Changed
|
||||
* Fix custom message format.
|
||||
@ -452,7 +815,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Only one crash from second instance.
|
||||
* During event `MessageID` in log as field.
|
||||
|
||||
## [v1.1.4 live] - 2019-04-10 (beta 2019-04-05, 2019-03-27)
|
||||
## [Bridge 1.1.4 live] - 2019-04-10 (beta 2019-04-05, 2019-03-27)
|
||||
|
||||
### Added
|
||||
* Address with port to IMAP debug.
|
||||
@ -473,7 +836,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
### Removed
|
||||
* #750 Synchronization after 450 messages.
|
||||
|
||||
## [v1.1.3] - 2019-03-04
|
||||
## [Bridge 1.1.3] - 2019-03-04
|
||||
|
||||
### Added
|
||||
* Sentry crash reporting in main.
|
||||
@ -485,13 +848,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* #720 sync every 3 pages.
|
||||
* #512 extending list of charsets go-pm-mime!4.
|
||||
|
||||
## [v1.1.2] - beta only 2019-02-21
|
||||
## [Bridge 1.1.2] - beta only 2019-02-21
|
||||
|
||||
### Changed
|
||||
* #512 fail on unknown charset.
|
||||
* #729 #733 visitor for MIME parsing.
|
||||
|
||||
## [v1.1.1] - 2019-02-11
|
||||
## [Bridge 1.1.1] - 2019-02-11
|
||||
### Added
|
||||
* #671 include `name` param in attachment `Content-Type` (in addition to `Content-Disposition` param `filename`).
|
||||
* #671 do not include content headers for section requests e.g. `BODY.PEEK[2]`.
|
||||
@ -544,7 +907,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* SMTP stays authenticated after sent message.
|
||||
* Reduce memory, processor and number of API calls.
|
||||
|
||||
## [v1.1.0] - 2018-10-22
|
||||
## [Bridge 1.1.0] - 2018-10-22
|
||||
|
||||
### Removed
|
||||
* `go-pmapi.Config.ClientSecret`.
|
||||
@ -620,11 +983,11 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Additional synchronization of mail database.
|
||||
|
||||
|
||||
## [v1.0.6 silent] - 2018-08-23
|
||||
## [Bridge 1.0.6 silent] - 2018-08-23
|
||||
### Added
|
||||
* New svg icon in linux package.
|
||||
|
||||
## [v1.0.6] - 2018-08-09
|
||||
## [Bridge 1.0.6] - 2018-08-09
|
||||
|
||||
### Added
|
||||
* `backend.GetUserSettings()`.
|
||||
@ -653,7 +1016,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Frequent Thunderbird timeout.
|
||||
* SMTP requests not case-sensitive.
|
||||
|
||||
## [v1.0.5] - 2018-07-12
|
||||
## [Bridge 1.0.5] - 2018-07-12
|
||||
|
||||
### Added
|
||||
* UpdateCurrentAgent from lastMailClient.
|
||||
@ -685,7 +1048,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Fixed 7bit MIME issue while sending.
|
||||
|
||||
|
||||
## [v1.0.4] - 2018-05-15
|
||||
## [Bridge 1.0.4] - 2018-05-15
|
||||
|
||||
### Changed
|
||||
* Version files available at both download and static.
|
||||
@ -706,11 +1069,11 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Notification that outgoing email will be delivered as non-encrypted.
|
||||
* NOTE: Due to a change of the keychain format, you will need to add your account(s) to the Bridge after installing this version.
|
||||
|
||||
### Bugs fixed
|
||||
### Fixed bugs
|
||||
* Support accounts with same user names.
|
||||
* Support sending vCalendar event.
|
||||
|
||||
## [v1.0.3] - 2018-03-26
|
||||
## [Bridge 1.0.3] - 2018-03-26
|
||||
* All from silent updates plus following.
|
||||
|
||||
### Changed
|
||||
@ -745,7 +1108,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Remove firewall error message.
|
||||
|
||||
|
||||
## [v1.0.2] - 2018-03-12
|
||||
## [Bridge 1.0.2] - 2018-03-12
|
||||
* All from silent updates plus following.
|
||||
|
||||
### Added
|
||||
@ -767,7 +1130,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
|
||||
## [v1.0.1-4 (linux only)] Silent deploy - 2018-02-28
|
||||
## [Bridge 1.0.1-4 (linux only)] Silent deploy - 2018-02-28
|
||||
|
||||
### Changed
|
||||
* More similar look of window title bar to Windows 10 style.
|
||||
@ -791,14 +1154,14 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
|
||||
## [v1.0.1] Silent deploy - 2017-12-30
|
||||
## [Bridge 1.0.1] Silent deploy - 2017-12-30
|
||||
|
||||
### Changed
|
||||
* Fixed bug with parsing address list (CC became BCC).
|
||||
|
||||
|
||||
|
||||
## [v1.0.1] - 2017-12-20
|
||||
## [Bridge 1.0.1] - 2017-12-20
|
||||
|
||||
### Added
|
||||
* When current log file is more than 10MB open new one, checked every 15min.
|
||||
@ -830,7 +1193,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
|
||||
## [v1.0.0] - 2017-12-06
|
||||
## [Bridge 1.0.0] - 2017-12-06
|
||||
|
||||
### Added
|
||||
* Encoding support of message body, title items, attachment name, for all standard charsets.
|
||||
|
||||
208
Makefile
208
Makefile
@ -3,67 +3,128 @@ export GO111MODULE=on
|
||||
# By default, the target OS is the same as the host OS,
|
||||
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
|
||||
GOOS:=$(shell go env GOOS)
|
||||
TARGET_CMD?=Desktop-Bridge
|
||||
TARGET_OS?=${GOOS}
|
||||
|
||||
## Build
|
||||
.PHONY: build build-nogui check-has-go
|
||||
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
|
||||
|
||||
BRIDGE_VERSION?=$(shell git describe --abbrev=0 --tags)-git
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=1.6.3+git
|
||||
IE_APP_VERSION?=1.3.0+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
SRC_ICO:=logo.ico
|
||||
SRC_ICNS:=Bridge.icns
|
||||
SRC_SVG:=logo.svg
|
||||
TGT_ICNS:=Bridge.icns
|
||||
EXE_NAME:=proton-bridge
|
||||
CONFIGNAME:=bridge
|
||||
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
|
||||
EXE_NAME:=proton-ie
|
||||
CONFIGNAME:=importExport
|
||||
endif
|
||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
|
||||
BUILD_TAGS?=pmapi_prod
|
||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
||||
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
|
||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/pkg/constants.,Version=${BRIDGE_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/internal/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
||||
ifneq "${BUILD_LDFLAGS}" ""
|
||||
GO_LDFLAGS+= ${BUILD_LDFLAGS}
|
||||
GO_LDFLAGS+=${BUILD_LDFLAGS}
|
||||
endif
|
||||
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
|
||||
GO_LDFLAGS_LAUNCHER+=$(addprefix -X main.,ConfigName=${CONFIGNAME} ExeName=proton-${APP})
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
GO_LDFLAGS_LAUNCHER+=-H=windowsgui
|
||||
endif
|
||||
GO_LDFLAGS:=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS+= ${GO_LDFLAGS}
|
||||
BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
|
||||
|
||||
DEPLOY_DIR:=cmd/Desktop-Bridge/deploy
|
||||
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_NOGUI+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
||||
|
||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||
ICO_FILES:=
|
||||
EXE:=$(shell basename ${CURDIR})
|
||||
|
||||
DIRNAME:=$(shell basename ${CURDIR})
|
||||
EXE:=${EXE_NAME}
|
||||
EXE_QT:=${DIRNAME}
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
EXE:=${EXE}.exe
|
||||
ICO_FILES:=logo.ico icon.rc icon_windows.syso
|
||||
EXE_QT:=${EXE_QT}.exe
|
||||
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
|
||||
endif
|
||||
ifeq "${TARGET_OS}" "darwin"
|
||||
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
||||
EXE:=${EXE}.app/Contents/MacOS/${EXE}
|
||||
EXE:=${EXE}.app
|
||||
EXE_QT:=${EXE_QT}.app
|
||||
EXE_BINARY_DARWIN:=/Contents/MacOS/${EXE_NAME}
|
||||
endif
|
||||
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
|
||||
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
|
||||
EXE_QT_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE_QT}
|
||||
|
||||
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
|
||||
ifeq "${TARGET_CMD}" "Import-Export"
|
||||
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
|
||||
endif
|
||||
|
||||
ifdef QT_API
|
||||
VENDOR_TARGET:=prepare-vendor update-qt-docs
|
||||
else
|
||||
VENDOR_TARGET=update-vendor
|
||||
endif
|
||||
|
||||
build: ${TGZ_TARGET}
|
||||
|
||||
build-ie:
|
||||
TARGET_CMD=Import-Export $(MAKE) build
|
||||
|
||||
build-nogui:
|
||||
go build ${BUILD_FLAGS_NOGUI} -o Desktop-Bridge cmd/Desktop-Bridge/main.go
|
||||
go build ${BUILD_FLAGS_NOGUI} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
|
||||
|
||||
build-ie-nogui:
|
||||
TARGET_CMD=Import-Export $(MAKE) build-nogui
|
||||
|
||||
build-launcher:
|
||||
go build ${BUILD_FLAGS_LAUNCHER} -o launcher-${APP} cmd/launcher/main.go
|
||||
|
||||
build-launcher-ie:
|
||||
TARGET_CMD=Import-Export $(MAKE) build-launcher
|
||||
|
||||
versioner:
|
||||
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
|
||||
|
||||
hasher:
|
||||
go build -o hasher utils/hasher/main.go
|
||||
|
||||
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
|
||||
rm -f $@
|
||||
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${TARGET_OS}
|
||||
cd ${DEPLOY_DIR}/${TARGET_OS} && tar czf ../../../../$@ .
|
||||
|
||||
${DEPLOY_DIR}/linux: ${EXE_TARGET}
|
||||
cp -pf ./internal/frontend/share/icons/logo.svg ${DEPLOY_DIR}/linux/
|
||||
cp -pf ./internal/frontend/share/icons/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg
|
||||
cp -pf ./LICENSE ${DEPLOY_DIR}/linux/
|
||||
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
|
||||
cp -pf ./dist/${EXE_NAME}.desktop ${DEPLOY_DIR}/linux/
|
||||
|
||||
${DEPLOY_DIR}/darwin: ${EXE_TARGET}
|
||||
cp ./internal/frontend/share/icons/Bridge.icns ${DARWINAPP_CONTENTS}/Resources/
|
||||
cp -r "utils/addcert.scpt" ${DARWINAPP_CONTENTS}/Resources/
|
||||
if [ "${DIRNAME}" != "${EXE_NAME}" ]; then \
|
||||
mv ${EXE_TARGET}/Contents/MacOS/{${DIRNAME},${EXE_NAME}}; \
|
||||
perl -i -pe"s/>${DIRNAME}/>${EXE_NAME}/g" ${EXE_TARGET}/Contents/Info.plist; \
|
||||
fi
|
||||
cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${TGT_ICNS}
|
||||
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}"
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}"
|
||||
|
||||
${DEPLOY_DIR}/windows: ${EXE_TARGET}
|
||||
cp ./internal/frontend/share/icons/logo.ico ${DEPLOY_DIR}/windows/
|
||||
cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico
|
||||
cp LICENSE ${DEPLOY_DIR}/windows/
|
||||
|
||||
QT_BUILD_TARGET:=build desktop
|
||||
@ -73,25 +134,24 @@ ifneq "${GOOS}" "${TARGET_OS}"
|
||||
endif
|
||||
endif
|
||||
|
||||
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
|
||||
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET}
|
||||
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
|
||||
cp cmd/Desktop-Bridge/main.go .
|
||||
cp cmd/${TARGET_CMD}/main.go .
|
||||
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
|
||||
mv deploy cmd/Desktop-Bridge
|
||||
mv deploy cmd/${TARGET_CMD}
|
||||
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
|
||||
rm -rf ${TARGET_OS} main.go
|
||||
|
||||
logo.ico: ./internal/frontend/share/icons/logo.ico
|
||||
cp $^ .
|
||||
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
|
||||
cp $^ $@
|
||||
icon.rc: ./internal/frontend/share/icon.rc
|
||||
cp $^ .
|
||||
./internal/frontend/qt/icon_windows.syso: ./internal/frontend/share/icon.rc logo.ico
|
||||
icon_windows.syso: icon.rc logo.ico
|
||||
windres --target=pe-x86-64 -o $@ $<
|
||||
icon_windows.syso: ./internal/frontend/qt/icon_windows.syso
|
||||
cp $^ .
|
||||
|
||||
|
||||
## Rules for therecipe/qt
|
||||
.PHONY: prepare-vendor update-vendor
|
||||
.PHONY: prepare-vendor update-vendor update-qt-docs
|
||||
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
|
||||
|
||||
# vendor folder will be deleted by gomod hence we cache the big repo
|
||||
@ -116,10 +176,12 @@ prepare-vendor:
|
||||
update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
|
||||
${LINKCMD}
|
||||
|
||||
update-qt-docs:
|
||||
go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API)
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated
|
||||
LINTVER:="v1.27.0"
|
||||
LINTVER:="v1.29.0"
|
||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||
|
||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||
@ -137,7 +199,7 @@ install-go-mod-outdated:
|
||||
|
||||
|
||||
## Checks, mocks and docs
|
||||
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc
|
||||
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc release-notes
|
||||
check-has-go:
|
||||
@which go || (echo "Install Go-lang!" && exit 1)
|
||||
|
||||
@ -152,15 +214,24 @@ test: gofiles
|
||||
go test -coverprofile=/tmp/coverage.out -run=${TESTRUN} \
|
||||
./internal/api/... \
|
||||
./internal/bridge/... \
|
||||
./internal/config/... \
|
||||
./internal/constants/... \
|
||||
./internal/cookies/... \
|
||||
./internal/crash/... \
|
||||
./internal/events/... \
|
||||
./internal/frontend/autoconfig/... \
|
||||
./internal/frontend/cli/... \
|
||||
./internal/imap/... \
|
||||
./internal/importexport/... \
|
||||
./internal/locations/... \
|
||||
./internal/logging/... \
|
||||
./internal/metrics/... \
|
||||
./internal/preferences/... \
|
||||
./internal/smtp/... \
|
||||
./internal/store/... \
|
||||
./internal/transfer/... \
|
||||
./internal/updater/... \
|
||||
./internal/users/... \
|
||||
./internal/versioner/... \
|
||||
./pkg/...
|
||||
|
||||
bench:
|
||||
@ -172,18 +243,24 @@ coverage: test
|
||||
go tool cover -html=/tmp/coverage.out -o=coverage.html
|
||||
|
||||
mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
|
||||
|
||||
lint: lint-golang lint-license
|
||||
lint: lint-golang lint-license lint-changelog
|
||||
|
||||
lint-license:
|
||||
./utils/missing_license.sh check
|
||||
|
||||
lint-changelog:
|
||||
./utils/changelog_linter.sh Changelog.md
|
||||
./utils/changelog_linter.sh unreleased.md
|
||||
|
||||
lint-golang:
|
||||
which golangci-lint || $(MAKE) install-linter
|
||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||
golangci-lint run ./...
|
||||
|
||||
updates: install-go-mod-outdated
|
||||
@ -193,20 +270,28 @@ updates: install-go-mod-outdated
|
||||
doc:
|
||||
godoc -http=:6060
|
||||
|
||||
release-notes: release-notes/bridge_stable.html release-notes/bridge_early.html release-notes/ie_stable.html release-notes/ie_early.html
|
||||
|
||||
release-notes/%.html: release-notes/%.md
|
||||
./utils/release_notes.sh $^
|
||||
|
||||
.PHONY: gofiles
|
||||
# Following files are for the whole app so it makes sense to have them in bridge package.
|
||||
# (Options like cmd or internal were considered and bridge package is the best place for them.)
|
||||
gofiles: ./internal/bridge/credits.go ./internal/bridge/release_notes.go
|
||||
gofiles: ./internal/bridge/credits.go ./internal/importexport/credits.go
|
||||
./internal/bridge/credits.go: ./utils/credits.sh go.mod
|
||||
cd ./utils/ && ./credits.sh
|
||||
./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes.txt ./release-notes/bugs.txt
|
||||
cd ./utils/ && ./release-notes.sh
|
||||
cd ./utils/ && ./credits.sh bridge
|
||||
./internal/importexport/credits.go: ./utils/credits.sh go.mod
|
||||
cd ./utils/ && ./credits.sh importexport
|
||||
|
||||
|
||||
## Run and debug
|
||||
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug qmlpreview qt-fronted-clean clean
|
||||
VERBOSITY?=debug-client
|
||||
RUN_FLAGS:=-m -l=${VERBOSITY}
|
||||
.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
|
||||
|
||||
LOG?=debug
|
||||
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
||||
LOG_SMTP?=--log-smtp # empty to turn it off
|
||||
RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
|
||||
|
||||
run: run-nogui-cli
|
||||
|
||||
@ -216,24 +301,45 @@ run-qt-cli: ${EXE_TARGET}
|
||||
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
|
||||
|
||||
run-nogui: clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} | tee last.log
|
||||
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log
|
||||
run-nogui-cli: clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} -c
|
||||
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c
|
||||
|
||||
run-debug:
|
||||
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/Desktop-Bridge/main.go -- ${RUN_FLAGS}
|
||||
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS}
|
||||
|
||||
run-qml-preview:
|
||||
make -C internal/frontend/qt -f Makefile.local qmlpreview
|
||||
$(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview
|
||||
run-ie-qml-preview:
|
||||
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview
|
||||
|
||||
run-ie:
|
||||
TARGET_CMD=Import-Export $(MAKE) run
|
||||
run-ie-qt:
|
||||
TARGET_CMD=Import-Export $(MAKE) run-qt
|
||||
run-ie-nogui:
|
||||
TARGET_CMD=Import-Export $(MAKE) run-nogui
|
||||
|
||||
clean-frontend-qt:
|
||||
make -C internal/frontend/qt -f Makefile.local clean
|
||||
$(MAKE) -C internal/frontend/qt -f Makefile.local clean
|
||||
clean-frontend-qt-ie:
|
||||
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local clean
|
||||
clean-frontend-qt-common:
|
||||
$(MAKE) -C internal/frontend/qt-common -f Makefile.local clean
|
||||
|
||||
clean-vendor: clean-frontend-qt
|
||||
clean-vendor: clean-frontend-qt clean-frontend-qt-ie clean-frontend-qt-common
|
||||
rm -rf ./vendor
|
||||
|
||||
clean: clean-frontend-qt
|
||||
clean: clean-vendor
|
||||
rm -rf vendor-cache
|
||||
rm -rf cmd/Desktop-Bridge/deploy
|
||||
rm -f build last.log mem.pprof
|
||||
rm -rf cmd/Import-Export/deploy
|
||||
rm -f build last.log mem.pprof main.go
|
||||
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso
|
||||
rm -f release-notes/bridge.html
|
||||
rm -f release-notes/import-export.html
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate ./...
|
||||
$(MAKE) add-license
|
||||
|
||||
41
README.md
41
README.md
@ -1,13 +1,13 @@
|
||||
# ProtonMail Bridge
|
||||
# ProtonMail Bridge and Import Export app
|
||||
Copyright (c) 2020 Proton Technologies AG
|
||||
|
||||
This repository holds the ProtonMail Bridge application.
|
||||
This repository holds the ProtonMail Bridge and the ProtonMail Import-Export applications.
|
||||
For a detailed build information see [BUILDS](./BUILDS.md).
|
||||
For licensing information see [COPYING](./COPYING.md).
|
||||
The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md).
|
||||
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
|
||||
## Description
|
||||
## Description Bridge
|
||||
ProtonMail Bridge for e-mail clients.
|
||||
|
||||
When launched, Bridge will initialize local IMAP/SMTP servers and render
|
||||
@ -24,6 +24,30 @@ background.
|
||||
|
||||
More details [on the public website](https://protonmail.com/bridge).
|
||||
|
||||
## Description Import-Export app
|
||||
ProtonMail Import-Export app for importing and exporting messages.
|
||||
|
||||
To transfer messages, firstly log in using your ProtonMail credentials.
|
||||
For import, expand your account, and pick the address to which to import
|
||||
messages from IMAP server or local EML or MBOX files. For export, pick
|
||||
the whole account or only a specific address. Then, in both cases,
|
||||
configure transfer rules (match source and target mailboxes, set time
|
||||
range limits and so on) and hit start. Once the transfer is complete,
|
||||
check the results.
|
||||
|
||||
More details [on the public website](https://protonmail.com/import-export).
|
||||
|
||||
## Launchers
|
||||
Launchers are binaries used to run the ProtonMail Bridge or Import-Export apps.
|
||||
|
||||
Official distributions of the ProtonMail Bridge and Import-Export apps contain
|
||||
both a launcher and the app itself. The launcher is installed in a protected
|
||||
area of the system (i.e. an area accessible only with admin privileges) and is
|
||||
used to run the app. The launcher ensures that nobody tampered with the app's
|
||||
files by verifying their signature using a hardcoded public key. App files are
|
||||
placed in regular userspace and are signed by Proton's private key. This
|
||||
feature enables the app to securely update itself automatically without asking
|
||||
the user for a password.
|
||||
|
||||
## Keychain
|
||||
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
|
||||
@ -39,12 +63,13 @@ or
|
||||
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
|
||||
|
||||
### Dev build or run
|
||||
- `BRIDGE_VERSION`: set the bridge app version used during testing or building
|
||||
- `APP_VERSION`: set the bridge app version used during testing or building
|
||||
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
|
||||
- `VERBOSITY`: set log level used during test time and by the makefile
|
||||
|
||||
### Integration testing
|
||||
- `TEST_ENV`: set which env to use (fake or live)
|
||||
- `TEST_APP`: set which app to test (bridge or ie)
|
||||
- `TEST_ACCOUNTS`: set JSON file with configured accounts
|
||||
- `TAGS`: set build tags for tests
|
||||
- `FEATURES`: set feature dir, file or scenario to test
|
||||
@ -59,9 +84,9 @@ The database stores metadata necessary for presenting messages and mailboxes to
|
||||
|
||||
### 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`
|
||||
- Linux: `~/.config/protonmail/bridge/prefs.json`
|
||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/prefs.json`
|
||||
- Windows: `%APPDATA%\protonmail\bridge\prefs.json`
|
||||
|
||||
### IMAP Cache
|
||||
The currently subscribed mailboxes are held in a json file:
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
FROM gitlab.protontech.ch:4567/protonmail/ci-containers/go
|
||||
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y install openssh-client libsecret-1-dev libgl1-mesa-dev time connect-proxy
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -35,398 +35,40 @@ package main
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/smtp"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/args"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/bridge"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
|
||||
// Different number will drop old files and create new ones.
|
||||
const cacheVersion = "c11"
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
|
||||
|
||||
// How many crashes in a row.
|
||||
numberOfCrashes = 0 //nolint[gochecknoglobals]
|
||||
|
||||
// After how many crashes bridge gives up starting.
|
||||
maxAllowedCrashes = 10 //nolint[gochecknoglobals]
|
||||
const (
|
||||
appName = "ProtonMail Bridge"
|
||||
appUsage = "ProtonMail IMAP and SMTP Bridge"
|
||||
configName = "bridge"
|
||||
updateURLName = "bridge"
|
||||
keychainName = "bridge"
|
||||
cacheVersion = "c11"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := raven.SetDSN(constants.DSNSentry); err != nil {
|
||||
log.WithError(err).Errorln("Can not setup sentry DSN")
|
||||
}
|
||||
raven.SetRelease(constants.Revision)
|
||||
|
||||
args.FilterProcessSerialNumberFromArgs()
|
||||
filterRestartNumberFromArgs()
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "Protonmail Bridge"
|
||||
app.Version = constants.BuildVersion
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "log-level, l",
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
|
||||
cli.BoolFlag{
|
||||
Name: "no-window",
|
||||
Usage: "Don't show window after start"},
|
||||
cli.BoolFlag{
|
||||
Name: "cli, c",
|
||||
Usage: "Use command line interface"},
|
||||
cli.BoolFlag{
|
||||
Name: "noninteractive",
|
||||
Usage: "Start Bridge entirely noninteractively"},
|
||||
cli.StringFlag{
|
||||
Name: "version-json, g",
|
||||
Usage: "Generate json version file"},
|
||||
cli.BoolFlag{
|
||||
Name: "mem-prof, m",
|
||||
Usage: "Generate memory profile"},
|
||||
cli.BoolFlag{
|
||||
Name: "cpu-prof, p",
|
||||
Usage: "Generate CPU profile"},
|
||||
}
|
||||
app.Usage = "ProtonMail IMAP and SMTP Bridge"
|
||||
app.Action = run
|
||||
|
||||
// Always log the basic info about current bridge.
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
log.WithField("version", constants.Version).
|
||||
WithField("revision", constants.Revision).
|
||||
WithField("runtime", runtime.GOOS).
|
||||
WithField("build", constants.BuildTime).
|
||||
WithField("args", os.Args).
|
||||
WithField("appLong", app.Name).
|
||||
WithField("appShort", constants.AppShortName).
|
||||
Info("Run app")
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Error("Program exited with error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
type panicHandler struct {
|
||||
cfg *config.Config
|
||||
err *error // Pointer to error of cli action.
|
||||
}
|
||||
|
||||
func (ph *panicHandler) HandlePanic() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r))
|
||||
frontend.HandlePanic()
|
||||
|
||||
*ph.err = cli.NewExitError("Panic and restart", 666)
|
||||
numberOfCrashes++
|
||||
log.Error("Restarting after panic")
|
||||
restartApp()
|
||||
os.Exit(666)
|
||||
}
|
||||
|
||||
// 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(constants.AppShortName, constants.Version, constants.Revision, cacheVersion)
|
||||
|
||||
// We want to know about any problem. Our PanicHandler calls sentry which is
|
||||
// not dependent on anything else. If that fails, it tries to create crash
|
||||
// report which will not be possible if no folder can be created. That's the
|
||||
// only problem we will not be notified about in any way.
|
||||
panicHandler := &panicHandler{cfg, &contextError}
|
||||
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")
|
||||
debugClient, debugServer := config.SetupLog(cfg, logLevel)
|
||||
|
||||
// Should be called after logs are configured but before preferences are created.
|
||||
migratePreferencesFromC10(cfg)
|
||||
|
||||
if err := cfg.ClearOldData(); err != nil {
|
||||
log.Error("Cannot clear old data: ", err)
|
||||
}
|
||||
|
||||
// Doesn't make sense to continue when Bridge was invoked with wrong arguments.
|
||||
// We should tell that to the user before we do anything else.
|
||||
if context.Args().First() != "" {
|
||||
_ = 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 Bridge instance).
|
||||
updates := updates.New(
|
||||
constants.AppShortName,
|
||||
constants.Version,
|
||||
constants.Revision,
|
||||
constants.BuildTime,
|
||||
bridge.ReleaseNotes,
|
||||
bridge.ReleaseFixedBugs,
|
||||
cfg.GetUpdateDir(),
|
||||
base, err := base.New(
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion,
|
||||
)
|
||||
|
||||
if dir := context.GlobalString("version-json"); dir != "" {
|
||||
generateVersionFiles(updates, dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearOldData before starting new bridge to do a proper setup.
|
||||
//
|
||||
// IMPORTANT: If you the change position of this you will need to wait
|
||||
// until force-update to be applied on all currently used bridge
|
||||
// versions
|
||||
if err := cfg.ClearOldData(); err != nil {
|
||||
log.Error("Cannot clear old data: ", err)
|
||||
}
|
||||
|
||||
// GetTLSConfig is needed for IMAP, SMTL and local bridge API (to check second instance).
|
||||
//
|
||||
// This should be called after ClearOldData, in order to re-create the
|
||||
// certificates if clean data will remove them (accidentally or on purpose).
|
||||
tls, err := config.GetTLSConfig(cfg)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Cannot get TLS certificate")
|
||||
logrus.WithError(err).Fatal("Failed to create app base")
|
||||
}
|
||||
|
||||
pref := preferences.New(cfg)
|
||||
|
||||
// Now we can try to proceed with starting the bridge. 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("Bridge is already running")
|
||||
if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil {
|
||||
numberOfCrashes = maxAllowedCrashes
|
||||
log.Error("Second instance: ", err)
|
||||
}
|
||||
return cli.NewExitError("Bridge is already running.", 3)
|
||||
}
|
||||
defer lock.Close() //nolint[errcheck]
|
||||
|
||||
// In case user wants to do CPU or memory profiles...
|
||||
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
|
||||
f, err := os.Create("cpu.pprof")
|
||||
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()
|
||||
}
|
||||
|
||||
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
|
||||
defer makeMemoryProfile()
|
||||
}
|
||||
|
||||
// Now we initialize all Bridge parts.
|
||||
log.Debug("Initializing bridge...")
|
||||
eventListener := listener.New()
|
||||
events.SetupEvents(eventListener)
|
||||
|
||||
credentialsStore, credentialsError := credentials.NewStore("bridge")
|
||||
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))
|
||||
|
||||
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, cm, credentialsStore)
|
||||
imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance)
|
||||
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
apiServer := api.NewAPIServer(pref, tls, cfg.GetTLSCertPath(), cfg.GetTLSKeyPath(), eventListener)
|
||||
apiServer.ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
imapPort := pref.GetInt(preferences.IMAPPortKey)
|
||||
imapServer := imap.NewIMAPServer(debugClient, debugServer, imapPort, tls, imapBackend, eventListener)
|
||||
imapServer.ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
smtpPort := pref.GetInt(preferences.SMTPPortKey)
|
||||
useSSL := pref.GetBool(preferences.SMTPSSLKey)
|
||||
smtpServer := smtp.NewSMTPServer(debugClient || debugServer, smtpPort, useSSL, tls, smtpBackend, eventListener)
|
||||
smtpServer.ListenAndServe()
|
||||
}()
|
||||
|
||||
// Decide about frontend mode before initializing rest of bridge.
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case context.GlobalBool("cli"):
|
||||
frontendMode = "cli"
|
||||
case context.GlobalBool("noninteractive"):
|
||||
frontendMode = "noninteractive"
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
}
|
||||
|
||||
log.WithField("mode", frontendMode).Debug("Determined frontend mode to use")
|
||||
|
||||
// If we are starting bridge in noninteractive mode, simply block instead of starting a frontend.
|
||||
if frontendMode == "noninteractive" {
|
||||
<-(make(chan struct{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
showWindowOnStart := !context.GlobalBool("no-window")
|
||||
frontend := frontend.New(constants.Version, constants.BuildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
|
||||
|
||||
// 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() {
|
||||
restartApp()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migratePreferencesFromC10 will copy preferences from c10 folder to c11.
|
||||
// It will happen only when c10/prefs.json exists and c11/prefs.json not.
|
||||
// No configuration changed between c10 and c11 versions.
|
||||
func migratePreferencesFromC10(cfg *config.Config) {
|
||||
pref10Path := config.New(constants.AppShortName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
|
||||
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
|
||||
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
|
||||
// Other instance already running.
|
||||
if base == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pref11Path := cfg.GetPreferencesPath()
|
||||
if _, err := os.Stat(pref11Path); err == nil {
|
||||
log.WithField("path", pref11Path).Trace("New preferences already exists, migration skipped")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(pref10Path) //nolint[gosec]
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Problem to load old preferences")
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(pref11Path, data, 0600)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Problem to migrate preferences")
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if err := bridge.New(base).Run(os.Args); err != nil {
|
||||
logrus.WithError(err).Fatal("Bridge exited with error")
|
||||
}
|
||||
}
|
||||
|
||||
57
cmd/Import-Export/main.go
Normal file
57
cmd/Import-Export/main.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2021 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 (
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/ie"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "ProtonMail Import-Export app"
|
||||
appUsage = "Import and export messages to/from your ProtonMail account"
|
||||
configName = "importExport"
|
||||
updateURLName = "ie"
|
||||
keychainName = "import-export-app"
|
||||
cacheVersion = "c11"
|
||||
)
|
||||
|
||||
func main() {
|
||||
base, err := base.New(
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create app base")
|
||||
}
|
||||
// Other instance already running.
|
||||
if base == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := ie.New(base).Run(os.Args); err != nil {
|
||||
logrus.WithError(err).Fatal("IE exited with error")
|
||||
}
|
||||
}
|
||||
193
cmd/launcher/main.go
Normal file
193
cmd/launcher/main.go
Normal file
@ -0,0 +1,193 @@
|
||||
// Copyright (c) 2021 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const appName = "ProtonMail Launcher"
|
||||
|
||||
var (
|
||||
ConfigName = "" // nolint[gochecknoglobals]
|
||||
ExeName = "" // nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
func main() { // nolint[funlen]
|
||||
reporter := sentry.NewReporter(appName, constants.Version)
|
||||
|
||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, ConfigName))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to get locations provider")
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, ConfigName)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to get logs path")
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := logging.Init(logsPath); err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to setup logging")
|
||||
}
|
||||
|
||||
logging.SetLevel(os.Getenv("VERBOSITY"))
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to get updates path")
|
||||
}
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create new verification key")
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create new verification keyring")
|
||||
}
|
||||
|
||||
versioner := versioner.New(updatesPath)
|
||||
|
||||
exe, err := getPathToExecutable(ExeName, versioner, kr, reporter)
|
||||
if err != nil {
|
||||
if exe, err = getFallbackExecutable(ExeName, versioner); err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to find any launchable executable")
|
||||
}
|
||||
}
|
||||
|
||||
launcher, err := os.Executable()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to determine path to launcher")
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, appendLauncherPath(launcher, os.Args[1:])...) // nolint[gosec]
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// On windows, if you use Run(), a terminal stays open; we don't want that.
|
||||
if runtime.GOOS == "windows" {
|
||||
err = cmd.Start()
|
||||
} else {
|
||||
err = cmd.Run()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to launch")
|
||||
}
|
||||
}
|
||||
|
||||
func appendLauncherPath(path string, args []string) []string {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
hasFlag := false
|
||||
|
||||
for k, v := range res {
|
||||
if v != "--launcher" {
|
||||
continue
|
||||
}
|
||||
|
||||
hasFlag = true
|
||||
|
||||
if k+1 >= len(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
res[k+1] = path
|
||||
}
|
||||
|
||||
if !hasFlag {
|
||||
res = append(res, "--launcher", path)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getPathToExecutable(
|
||||
name string,
|
||||
versioner *versioner.Versioner,
|
||||
kr *crypto.KeyRing,
|
||||
reporter *sentry.Reporter,
|
||||
) (string, error) {
|
||||
versions, err := versioner.ListVersions()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to list available versions")
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
vlog := logrus.WithField("version", version)
|
||||
|
||||
if err := version.VerifyFiles(kr); err != nil {
|
||||
vlog.WithError(err).Error("Files failed verification and will be removed")
|
||||
|
||||
if err := reporter.ReportMessage(fmt.Sprintf("version %v failed verification: %v", version, err)); err != nil {
|
||||
vlog.WithError(err).Error("Failed to report corrupt update files")
|
||||
}
|
||||
|
||||
if err := version.Remove(); err != nil {
|
||||
vlog.WithError(err).Error("Failed to remove files")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
exe, err := version.GetExecutable(name)
|
||||
if err != nil {
|
||||
vlog.WithError(err).Error("Failed to get executable")
|
||||
continue
|
||||
}
|
||||
|
||||
return exe, nil
|
||||
}
|
||||
|
||||
return "", errors.New("no available versions")
|
||||
}
|
||||
|
||||
func getFallbackExecutable(name string, versioner *versioner.Versioner) (string, error) {
|
||||
logrus.Info("Searching for fallback executable")
|
||||
|
||||
launcher, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to determine path to launcher")
|
||||
}
|
||||
|
||||
return versioner.GetExecutableInDirectory(name, filepath.Dir(launcher))
|
||||
}
|
||||
11
dist/proton-bridge.desktop
vendored
Normal file
11
dist/proton-bridge.desktop
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.1
|
||||
Name=ProtonMail Bridge
|
||||
GenericName=ProtonMail Bridge for Linux
|
||||
Comment=The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer.
|
||||
Icon=protonmail-bridge
|
||||
Exec=protonmail-bridge
|
||||
Terminal=false
|
||||
Categories=Office;Email;Network
|
||||
StartupWMClass=protonmail-bridge
|
||||
11
dist/proton-ie.desktop
vendored
Normal file
11
dist/proton-ie.desktop
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.1
|
||||
Name=ProtonMail Import-Export app
|
||||
GenericName=ProtonMail Import-Export app for Linux
|
||||
Comment=The Import-Export app helps you to migrate your emails from local files or remote IMAP servers to ProtonMail or simply export emails to local folder.
|
||||
Icon=protonmail-import-export-app
|
||||
Exec=protonmail-import-export-app
|
||||
Terminal=false
|
||||
Categories=Office;Email;Network
|
||||
StartupWMClass=protonmail-import-export-app
|
||||
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:
|
||||
|
||||
* [Development cycle](development.md)
|
||||
## Bridge
|
||||
|
||||
* [Bridge code](bridge.md)
|
||||
* [Internal Bridge database](database.md)
|
||||
* [Communication between Bridge, Client and Server](communication.md)
|
||||
* [Encryption](encryption.md)
|
||||
|
||||
## Import-Export app
|
||||
|
||||
* [Import-Export code](importexport.md)
|
||||
|
||||
69
go.mod
69
go.mod
@ -6,70 +6,69 @@ go 1.13
|
||||
// They are in a separate require block to highlight this.
|
||||
require (
|
||||
github.com/docker/docker-credential-helpers v0.6.3
|
||||
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
|
||||
github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df
|
||||
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
|
||||
github.com/ProtonMail/go-appdir v1.1.0
|
||||
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
|
||||
github.com/Masterminds/semver/v3 v3.1.0
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/ProtonMail/go-rfc5322 v0.5.0
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.0.1
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.3
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
|
||||
github.com/andybalholm/cascadia v1.1.0
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/cucumber/godog v0.8.1
|
||||
github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
|
||||
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
|
||||
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
|
||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
|
||||
github.com/emersion/go-mbox v1.0.2
|
||||
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||
github.com/emersion/go-smtp v0.14.0
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/getsentry/raven-go v0.2.0
|
||||
github.com/go-resty/resty/v2 v2.2.0
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/getsentry/sentry-go v0.8.0
|
||||
github.com/go-resty/resty/v2 v2.3.0
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/google/go-cmp v0.5.1
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
|
||||
github.com/hashicorp/go-multierror v1.0.0
|
||||
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195
|
||||
github.com/jhillyerd/enmime v0.8.0
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
|
||||
github.com/miekg/dns v1.1.29
|
||||
github.com/myesui/uuid v1.0.0 // indirect
|
||||
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
|
||||
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41
|
||||
github.com/twinj/uuid v1.0.0 // indirect
|
||||
github.com/urfave/cli v1.22.3
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/vmihailenco/msgpack/v5 v5.1.3
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381
|
||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||
github.com/emersion/go-imap => github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843
|
||||
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
|
||||
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac
|
||||
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
|
||||
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c
|
||||
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c
|
||||
)
|
||||
|
||||
328
go.sum
328
go.sum
@ -1,46 +1,64 @@
|
||||
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/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
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/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
|
||||
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
|
||||
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c h1:iaVbEOnskSGgcH7XQWHG6VPirHDRoYe+Idd0/dl4m8A=
|
||||
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
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-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-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-crypto v0.0.0-20201208171014-cdb7591792e2 h1:pQkjJELHayW59jp7r4G5Dlmnicr5McejDfwsjcwI1SU=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2/go.mod h1:HTM9X7e9oLwn7RiqLG0UVwVRJenLs3wN+tQ0NPAfwMQ=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/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/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
|
||||
github.com/ProtonMail/go-rfc5322 v0.5.0 h1:LbKWjgfvumYZCr8BgGyTUk3ETGkFLAjQdkuSUpZ5CcE=
|
||||
github.com/ProtonMail/go-rfc5322 v0.5.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
|
||||
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/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.3 h1:4+nFDJ9WtcUQTip/je2Ll3P21XhAUl4asWsafLrw97c=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.3/go.mod h1:WeYndoqEcRR4/QbgRL24z6OwYX5T1RWerRk8NfZ6rJM=
|
||||
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/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
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/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/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/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY=
|
||||
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8=
|
||||
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
|
||||
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
|
||||
@ -48,6 +66,11 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
|
||||
@ -56,90 +79,175 @@ github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1Po
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
|
||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
|
||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
|
||||
github.com/emersion/go-imap-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-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac=
|
||||
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
|
||||
github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
||||
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
|
||||
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b h1:xYuhW6egTaCP+zjbUcfoy/Dr3ASdVPR9W7fmkHvZHPE=
|
||||
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
|
||||
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-smtp v0.14.0 h1:RYW203p+EcPjL8Z/ZpT9lZ6iOc8MG1MQzEx1UKEkXlA=
|
||||
github.com/emersion/go-smtp v0.14.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/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/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
|
||||
github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
|
||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/golang/mock v1.4.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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.8.0 h1:F52cjBVLuiTfdW6p4JFuxlt3pOjKfWYT/aka7cdJ7v0=
|
||||
github.com/getsentry/sentry-go v0.8.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
|
||||
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/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/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
|
||||
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195 h1:j0UEFmS7wSjAwKEIkgKBn8PRDfjcuggzr93R9wk53nQ=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jhillyerd/enmime v0.8.0 h1:PHc/2LXtnDmCDm0V4+5NlBx+MoubmufhuNXwpKSV2o8=
|
||||
github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2 h1:1XZArHAPddaXKbg51etNbCjkNUkKgSa0s8dSz2LYB2g=
|
||||
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
|
||||
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
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/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/martinlindhe/base36 v1.1.0 h1:cIwvvwYse/0+1CkUPYH5ZvVIYG3JrILmQEIbLuar02Y=
|
||||
github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.7/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.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
|
||||
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
|
||||
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6 h1:qsqscDgSJy+HqgMTR+3NwjYJBbp1+honwDsszLoS+pA=
|
||||
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
|
||||
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
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/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.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -149,57 +257,111 @@ 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
|
||||
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
||||
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
|
||||
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
|
||||
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
|
||||
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/vmihailenco/msgpack/v5 v5.1.3 h1:FwC9KPjyW8OqTUqMt6rQw9y50vA2cTLXPKCcBCRbQgg=
|
||||
github.com/vmihailenco/msgpack/v5 v5.1.3/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI=
|
||||
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
||||
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190327091125-710a502c58a2/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-20190503192946-f4e77d36d62c/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-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
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.5-0.20201125200606-c27b9fd57aec h1:A1qYjneJuzBZZ2gIB8rd6zrfq6l7SoEMJ8EsSilNK/U=
|
||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/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-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/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-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
|
||||
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -22,14 +22,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -41,21 +39,15 @@ var (
|
||||
|
||||
type apiServer struct {
|
||||
host string
|
||||
pref *config.Preferences
|
||||
tls *tls.Config
|
||||
certPath string
|
||||
keyPath string
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
}
|
||||
|
||||
// NewAPIServer returns prepared API server struct.
|
||||
func NewAPIServer(pref *config.Preferences, tls *tls.Config, certPath, keyPath string, eventListener listener.Listener) *apiServer { //nolint[golint]
|
||||
func NewAPIServer(settings *settings.Settings, eventListener listener.Listener) *apiServer { //nolint[golint]
|
||||
return &apiServer{
|
||||
host: bridge.Host,
|
||||
pref: pref,
|
||||
tls: tls,
|
||||
certPath: certPath,
|
||||
keyPath: keyPath,
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
}
|
||||
}
|
||||
@ -67,14 +59,12 @@ func (api *apiServer) ListenAndServe() {
|
||||
|
||||
addr := api.getAddress()
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
TLSConfig: api.tls,
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
log.Info("API listening at ", addr)
|
||||
if err := server.ListenAndServeTLS(api.certPath, api.keyPath); err != nil {
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
|
||||
log.Error("API failed: ", err)
|
||||
}
|
||||
@ -82,10 +72,10 @@ func (api *apiServer) ListenAndServe() {
|
||||
}
|
||||
|
||||
func (api *apiServer) getAddress() string {
|
||||
port := api.pref.GetInt(preferences.APIPortKey)
|
||||
port := api.settings.GetInt(settings.APIPortKey)
|
||||
newPort := ports.FindFreePortFrom(port)
|
||||
if newPort != port {
|
||||
api.pref.SetInt(preferences.APIPortKey, newPort)
|
||||
api.settings.SetInt(settings.APIPortKey, newPort)
|
||||
}
|
||||
return getAPIAddress(api.host, newPort)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -18,7 +18,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
@ -37,12 +36,9 @@ func focusHandler(ctx handlerContext) error {
|
||||
|
||||
// CheckOtherInstanceAndFocus is helper for new instances to check if there is
|
||||
// already a running instance and get it's focus.
|
||||
func CheckOtherInstanceAndFocus(port int, tls *tls.Config) error {
|
||||
transport := &http.Transport{TLSClientConfig: tls}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
func CheckOtherInstanceAndFocus(port int) error {
|
||||
addr := getAPIAddress(bridge.Host, port)
|
||||
resp, err := client.Get("https://" + addr + "/focus")
|
||||
resp, err := (&http.Client{}).Get("http://" + addr + "/focus")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -15,21 +15,21 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package args
|
||||
package base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
import "strings"
|
||||
|
||||
// FilterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber
|
||||
// StripProcessSerialNumber removes additional flag from macOS.
|
||||
// More info:
|
||||
// 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() {
|
||||
tmp := os.Args[:0]
|
||||
for _, arg := range os.Args {
|
||||
func StripProcessSerialNumber(args []string) []string {
|
||||
res := args[:0]
|
||||
|
||||
for _, arg := range args {
|
||||
if !strings.Contains(arg, "-psn_") {
|
||||
tmp = append(tmp, arg)
|
||||
res = append(res, arg)
|
||||
}
|
||||
}
|
||||
os.Args = tmp
|
||||
|
||||
return res
|
||||
}
|
||||
366
internal/app/base/base.go
Normal file
366
internal/app/base/base.go
Normal file
@ -0,0 +1,366 @@
|
||||
// Copyright (c) 2021 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 base implements a common application base currently shared by bridge and IE.
|
||||
// The base includes the following:
|
||||
// - access to standard filesystem locations like config, cache, logging dirs
|
||||
// - an extensible crash handler
|
||||
// - versioned cache directory
|
||||
// - persistent settings
|
||||
// - event listener
|
||||
// - credentials store
|
||||
// - pmapi ClientManager
|
||||
// In addition, the base initialises logging and reacts to command line arguments
|
||||
// which control the log verbosity and enable cpu/memory profiling.
|
||||
package base
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/cache"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/tls"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
CrashHandler *crash.Handler
|
||||
Locations *locations.Locations
|
||||
Settings *settings.Settings
|
||||
Lock *os.File
|
||||
Cache *cache.Cache
|
||||
Listener listener.Listener
|
||||
Creds *credentials.Store
|
||||
CM *pmapi.ClientManager
|
||||
CookieJar *cookies.Jar
|
||||
Updater *updater.Updater
|
||||
Versioner *versioner.Versioner
|
||||
TLS *tls.TLS
|
||||
Autostart *autostart.App
|
||||
|
||||
Name string // the app's name
|
||||
usage string // the app's usage description
|
||||
command string // the command used to launch the app (either the exe path or the launcher path)
|
||||
restart bool // whether the app is currently set to restart
|
||||
|
||||
teardown []func() error // actions to perform when app is exiting
|
||||
}
|
||||
|
||||
func New( // nolint[funlen]
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion string,
|
||||
) (*Base, error) {
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version)
|
||||
crashHandler := crash.NewHandler(
|
||||
sentryReporter.ReportException,
|
||||
crash.ShowErrorNotification(appName),
|
||||
)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
os.Args = StripProcessSerialNumber(os.Args)
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := logging.Init(logsPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := migrateFiles(configName); err != nil {
|
||||
logrus.WithError(err).Warn("Old config files could not be migrated")
|
||||
}
|
||||
|
||||
if err := locations.Clean(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settingsPath, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settingsObj := settings.New(settingsPath)
|
||||
|
||||
lock, err := singleinstance.CreateLockFile(locations.GetLockFile())
|
||||
if err != nil {
|
||||
logrus.Warnf("%v is already running", appName)
|
||||
return nil, api.CheckOtherInstanceAndFocus(settingsObj.GetInt(settings.APIPortKey))
|
||||
}
|
||||
|
||||
cachePath, err := locations.ProvideCachePath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache, err := cache.New(cachePath, cacheVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cache.RemoveOldVersions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listener := listener.New()
|
||||
events.SetupEvents(listener)
|
||||
|
||||
// If we can't load the keychain for whatever reason,
|
||||
// we signal to frontend and supply a dummy keychain that always returns errors.
|
||||
kc, err := keychain.NewKeychain(settingsObj, keychainName)
|
||||
if err != nil {
|
||||
listener.Emit(events.CredentialsErrorEvent, err.Error())
|
||||
kc = keychain.NewMissingKeychain()
|
||||
}
|
||||
|
||||
jar, err := cookies.NewCookieJar(settingsObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiConfig := pmapi.GetAPIConfig(configName, constants.Version)
|
||||
apiConfig.ConnectionOffHandler = func() {
|
||||
listener.Emit(events.InternetOffEvent, "")
|
||||
}
|
||||
apiConfig.ConnectionOnHandler = func() {
|
||||
listener.Emit(events.InternetOnEvent, "")
|
||||
}
|
||||
apiConfig.UpgradeApplicationHandler = func() {
|
||||
listener.Emit(events.UpgradeApplicationEvent, "")
|
||||
}
|
||||
cm := pmapi.NewClientManager(apiConfig)
|
||||
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
|
||||
cm.SetCookieJar(jar)
|
||||
sentryReporter.SetUserAgentProvider(cm)
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatesDir, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versioner := versioner.New(updatesDir)
|
||||
installer := updater.NewInstaller(versioner)
|
||||
updater := updater.New(
|
||||
cm,
|
||||
installer,
|
||||
settingsObj,
|
||||
kr,
|
||||
semver.MustParse(constants.Version),
|
||||
updateURLName,
|
||||
runtime.GOOS,
|
||||
)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
autostart := &autostart.App{
|
||||
Name: appName,
|
||||
DisplayName: appName,
|
||||
Exec: []string{exe},
|
||||
}
|
||||
|
||||
return &Base{
|
||||
CrashHandler: crashHandler,
|
||||
Locations: locations,
|
||||
Settings: settingsObj,
|
||||
Lock: lock,
|
||||
Cache: cache,
|
||||
Listener: listener,
|
||||
Creds: credentials.NewStore(kc),
|
||||
CM: cm,
|
||||
CookieJar: jar,
|
||||
Updater: updater,
|
||||
Versioner: versioner,
|
||||
TLS: tls.New(settingsPath),
|
||||
Autostart: autostart,
|
||||
|
||||
Name: appName,
|
||||
usage: appUsage,
|
||||
|
||||
// By default, the command is the app's executable.
|
||||
// This can be changed at runtime by using the "--launcher" flag.
|
||||
command: exe,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *Base) NewApp(action func(*Base, *cli.Context) error) *cli.App {
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = b.Name
|
||||
app.Usage = b.usage
|
||||
app.Version = constants.Version
|
||||
app.Action = b.run(action)
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "cpu-prof",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "mem-prof",
|
||||
Aliases: []string{"m"},
|
||||
Usage: "Generate memory profile",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "cli",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "Use command line interface",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "restart",
|
||||
Usage: "The number of times the application has already restarted",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "launcher",
|
||||
Usage: "The launcher to use to restart the application",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// SetToRestart sets the app to restart the next time it is closed.
|
||||
func (b *Base) SetToRestart() {
|
||||
b.restart = true
|
||||
}
|
||||
|
||||
// AddTeardownAction adds an action to perform during app teardown.
|
||||
func (b *Base) AddTeardownAction(fn func() error) {
|
||||
b.teardown = append(b.teardown, fn)
|
||||
}
|
||||
|
||||
func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc { // nolint[funlen]
|
||||
return func(c *cli.Context) error {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
defer func() { _ = b.Lock.Close() }()
|
||||
|
||||
// If launcher was used to start the app, use that for restart/autostart.
|
||||
if launcher := c.String("launcher"); launcher != "" {
|
||||
b.Autostart.Exec = []string{launcher}
|
||||
b.command = launcher
|
||||
}
|
||||
|
||||
if doCPUProfile := c.Bool("cpu-prof"); doCPUProfile {
|
||||
startCPUProfile()
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if doMemoryProfile := c.Bool("mem-prof"); doMemoryProfile {
|
||||
defer makeMemoryProfile()
|
||||
}
|
||||
|
||||
logging.SetLevel(c.String("log-level"))
|
||||
|
||||
logrus.
|
||||
WithField("appName", b.Name).
|
||||
WithField("version", constants.Version).
|
||||
WithField("revision", constants.Revision).
|
||||
WithField("build", constants.BuildTime).
|
||||
WithField("runtime", runtime.GOOS).
|
||||
WithField("args", os.Args).
|
||||
Info("Run app")
|
||||
|
||||
b.CrashHandler.AddRecoveryAction(func(interface{}) error {
|
||||
if c.Int("restart") > maxAllowedRestarts {
|
||||
logrus.
|
||||
WithField("restart", c.Int("restart")).
|
||||
Warn("Not restarting, already restarted too many times")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.restartApp(true)
|
||||
})
|
||||
|
||||
if err := appMainLoop(b, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.doTeardown(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.restart {
|
||||
return b.restartApp(false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Base) doTeardown() error {
|
||||
for _, action := range b.teardown {
|
||||
if err := action(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
83
internal/app/base/migration.go
Normal file
83
internal/app/base/migration.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2021 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 base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// migrateFiles migrates files from their old (pre-refactor) locations to their new locations.
|
||||
// We can remove this eventually.
|
||||
//
|
||||
// | entity | old location | new location |
|
||||
// |--------|-------------------------------------------|----------------------------------------|
|
||||
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
|
||||
// | c11 | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
|
||||
func migrateFiles(configName string) error {
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
userCacheDir := locationsProvider.UserCache()
|
||||
newSettingsDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveIfExists(
|
||||
filepath.Join(userCacheDir, "c11", "prefs.json"),
|
||||
filepath.Join(newSettingsDir, "prefs.json"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newCacheDir, err := locations.ProvideCachePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveIfExists(
|
||||
filepath.Join(userCacheDir, "c11"),
|
||||
filepath.Join(newCacheDir, "c11"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveIfExists(source, destination string) error {
|
||||
if _, err := os.Stat(source); os.IsNotExist(err) {
|
||||
logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file")
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(destination); !os.IsNotExist(err) {
|
||||
logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file")
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
56
internal/app/base/profiling.go
Normal file
56
internal/app/base/profiling.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2021 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 base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// startCPUProfile starts CPU pprof.
|
||||
func startCPUProfile() {
|
||||
f, err := os.Create("./cpu.pprof")
|
||||
if err != nil {
|
||||
logrus.Fatal("Could not create CPU profile: ", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
logrus.Fatal("Could not start CPU profile: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// makeMemoryProfile generates memory pprof.
|
||||
func makeMemoryProfile() {
|
||||
name := "./mem.pprof"
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
logrus.Fatal("Could not create memory profile: ", err)
|
||||
}
|
||||
if abs, err := filepath.Abs(name); err == nil {
|
||||
name = abs
|
||||
}
|
||||
logrus.Info("Writing memory profile to ", name)
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
logrus.Fatal("Could not write memory profile: ", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
}
|
||||
79
internal/app/base/restart.go
Normal file
79
internal/app/base/restart.go
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2021 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 base
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// maxAllowedRestarts controls after how many crashes the app will give up restarting.
|
||||
const maxAllowedRestarts = 10
|
||||
|
||||
func (b *Base) restartApp(crash bool) error {
|
||||
var args []string
|
||||
|
||||
if crash {
|
||||
args = incrementRestartFlag(os.Args)[1:]
|
||||
} else {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
|
||||
logrus.
|
||||
WithField("command", b.command).
|
||||
WithField("args", args).
|
||||
Warn("Restarting")
|
||||
|
||||
return exec.Command(b.command, args...).Start() // nolint[gosec]
|
||||
}
|
||||
|
||||
// incrementRestartFlag increments the value of the restart flag.
|
||||
// If no such flag is present, it is added with initial value 1.
|
||||
func incrementRestartFlag(args []string) []string {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
hasFlag := false
|
||||
|
||||
for k, v := range res {
|
||||
if v != "--restart" {
|
||||
continue
|
||||
}
|
||||
|
||||
hasFlag = true
|
||||
|
||||
if k+1 >= len(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(res[k+1])
|
||||
if err != nil {
|
||||
res[k+1] = "1"
|
||||
} else {
|
||||
res[k+1] = strconv.Itoa(n + 1)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasFlag {
|
||||
res = append(res, "--restart", "1")
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
49
internal/app/base/restart_test.go
Normal file
49
internal/app/base/restart_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2021 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 base
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIncrementRestartFlag(t *testing.T) {
|
||||
var tests = []struct {
|
||||
in []string
|
||||
out []string
|
||||
}{
|
||||
{[]string{"./bridge", "--restart", "1"}, []string{"./bridge", "--restart", "2"}},
|
||||
{[]string{"./bridge", "--restart", "2"}, []string{"./bridge", "--restart", "3"}},
|
||||
{[]string{"./bridge", "--other", "--restart", "2"}, []string{"./bridge", "--other", "--restart", "3"}},
|
||||
{[]string{"./bridge", "--restart", "2", "--other"}, []string{"./bridge", "--restart", "3", "--other"}},
|
||||
{[]string{"./bridge", "--restart", "2", "--other", "2"}, []string{"./bridge", "--restart", "3", "--other", "2"}},
|
||||
{[]string{"./bridge"}, []string{"./bridge", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--something"}, []string{"./bridge", "--something", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--something", "--else"}, []string{"./bridge", "--something", "--else", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--restart", "bad"}, []string{"./bridge", "--restart", "1"}},
|
||||
{[]string{"./bridge", "--restart", "bad", "--other"}, []string{"./bridge", "--restart", "1", "--other"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.in, " "), func(t *testing.T) {
|
||||
assert.Equal(t, tt.out, incrementRestartFlag(tt.in))
|
||||
})
|
||||
}
|
||||
}
|
||||
226
internal/app/bridge/bridge.go
Normal file
226
internal/app/bridge/bridge.go
Normal file
@ -0,0 +1,226 @@
|
||||
// Copyright (c) 2021 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 implements the bridge CLI application.
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
pkgTLS "github.com/ProtonMail/proton-bridge/internal/config/tls"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap"
|
||||
"github.com/ProtonMail/proton-bridge/internal/smtp"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func New(base *base.Base) *cli.App {
|
||||
app := base.NewApp(run)
|
||||
|
||||
app.Flags = append(app.Flags, []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "log-imap",
|
||||
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"},
|
||||
&cli.BoolFlag{
|
||||
Name: "log-smtp",
|
||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)"},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-window",
|
||||
Usage: "Don't show window after start"},
|
||||
&cli.BoolFlag{
|
||||
Name: "noninteractive",
|
||||
Usage: "Start Bridge entirely noninteractively"},
|
||||
}...)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
tlsConfig, err := loadTLSConfig(b)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to load TLS config")
|
||||
}
|
||||
|
||||
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
|
||||
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, bridge)
|
||||
smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge)
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
api.NewAPIServer(b.Settings, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
imapPort := b.Settings.GetInt(settings.IMAPPortKey)
|
||||
imap.NewIMAPServer(
|
||||
b.CrashHandler,
|
||||
c.String("log-imap") == "client" || c.String("log-imap") == "all",
|
||||
c.String("log-imap") == "server" || c.String("log-imap") == "all",
|
||||
imapPort, tlsConfig, imapBackend, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
|
||||
useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
|
||||
smtp.NewSMTPServer(
|
||||
c.Bool("log-smtp"),
|
||||
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
// Bridge supports no-window option which we should use for autostart.
|
||||
b.Autostart.Exec = append(b.Autostart.Exec, "--no-window")
|
||||
|
||||
// We want to remove old versions if the app exits successfully.
|
||||
b.AddTeardownAction(b.Versioner.RemoveOldVersions)
|
||||
|
||||
// We want cookies to be saved to disk so they are loaded the next time.
|
||||
b.AddTeardownAction(b.CookieJar.PersistCookies)
|
||||
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case c.Bool("cli"):
|
||||
frontendMode = "cli"
|
||||
case c.Bool("noninteractive"):
|
||||
return <-(make(chan error)) // Block forever.
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
}
|
||||
|
||||
f := frontend.New(
|
||||
constants.Version,
|
||||
constants.BuildVersion,
|
||||
b.Name,
|
||||
frontendMode,
|
||||
!c.Bool("no-window"),
|
||||
b.CrashHandler,
|
||||
b.Locations,
|
||||
b.Settings,
|
||||
b.Listener,
|
||||
b.Updater,
|
||||
bridge,
|
||||
smtpBackend,
|
||||
b.Autostart,
|
||||
b,
|
||||
)
|
||||
|
||||
// Watch for updates routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
|
||||
for {
|
||||
checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey))
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
|
||||
return f.Loop()
|
||||
}
|
||||
|
||||
func loadTLSConfig(b *base.Base) (*tls.Config, error) {
|
||||
if !b.TLS.HasCerts() {
|
||||
if err := generateTLSCerts(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tlsConfig, err := b.TLS.GetConfig()
|
||||
if err == nil {
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
logrus.WithError(err).Error("Failed to load TLS config, regenerating certificates")
|
||||
|
||||
if err := generateTLSCerts(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.TLS.GetConfig()
|
||||
}
|
||||
|
||||
func generateTLSCerts(b *base.Base) error {
|
||||
template, err := pkgTLS.NewTLSTemplate()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate TLS template")
|
||||
}
|
||||
|
||||
if err := b.TLS.GenerateCerts(template); err != nil {
|
||||
return errors.Wrap(err, "failed to generate TLS certs")
|
||||
}
|
||||
|
||||
if err := b.TLS.InstallCerts(); err != nil {
|
||||
return errors.Wrap(err, "failed to install TLS certs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
|
||||
version, err := u.Check()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("An error occurred while checking for updates")
|
||||
return
|
||||
}
|
||||
|
||||
f.WaitUntilFrontendIsReady()
|
||||
|
||||
// Update links in UI
|
||||
f.SetVersion(version)
|
||||
|
||||
if !u.IsUpdateApplicable(version) {
|
||||
logrus.Debug("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("version", version.Version).Info("An update is available")
|
||||
|
||||
if !autoUpdate {
|
||||
f.NotifyManualUpdate(version, u.CanInstall(version))
|
||||
return
|
||||
}
|
||||
|
||||
if !u.CanInstall(version) {
|
||||
logrus.Info("A manual update is required")
|
||||
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.InstallUpdate(version); err != nil {
|
||||
if errors.Cause(err) == updater.ErrDownloadVerify {
|
||||
logrus.WithError(err).Warning("Skipping update installation due to temporary error")
|
||||
} else {
|
||||
logrus.WithError(err).Error("The update couldn't be installed")
|
||||
f.NotifySilentUpdateError(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f.NotifySilentUpdateInstalled()
|
||||
}
|
||||
133
internal/app/ie/ie.go
Normal file
133
internal/app/ie/ie.go
Normal file
@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2021 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 ie implements the ie CLI application.
|
||||
package ie
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func New(b *base.Base) *cli.App {
|
||||
return b.NewApp(run)
|
||||
}
|
||||
|
||||
func run(b *base.Base, c *cli.Context) error {
|
||||
ie := importexport.New(b.Locations, b.Cache, b.CrashHandler, b.Listener, b.CM, b.Creds)
|
||||
|
||||
go func() {
|
||||
defer b.CrashHandler.HandlePanic()
|
||||
api.NewAPIServer(b.Settings, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case c.Bool("cli"):
|
||||
frontendMode = "cli"
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
}
|
||||
|
||||
// We want to remove old versions if the app exits successfully.
|
||||
b.AddTeardownAction(b.Versioner.RemoveOldVersions)
|
||||
|
||||
// We want cookies to be saved to disk so they are loaded the next time.
|
||||
b.AddTeardownAction(b.CookieJar.PersistCookies)
|
||||
|
||||
f := frontend.NewImportExport(
|
||||
constants.Version,
|
||||
constants.BuildVersion,
|
||||
b.Name,
|
||||
frontendMode,
|
||||
b.CrashHandler,
|
||||
b.Locations,
|
||||
b.Settings,
|
||||
b.Listener,
|
||||
b.Updater,
|
||||
ie,
|
||||
b,
|
||||
)
|
||||
|
||||
// Watch for updates routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
|
||||
for {
|
||||
checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey))
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
|
||||
return f.Loop()
|
||||
}
|
||||
|
||||
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
|
||||
version, err := u.Check()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("An error occurred while checking for updates")
|
||||
return
|
||||
}
|
||||
|
||||
f.WaitUntilFrontendIsReady()
|
||||
|
||||
// Update links in UI
|
||||
f.SetVersion(version)
|
||||
|
||||
if !u.IsUpdateApplicable(version) {
|
||||
logrus.Debug("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("version", version.Version).Info("An update is available")
|
||||
|
||||
if !autoUpdate {
|
||||
f.NotifyManualUpdate(version, u.CanInstall(version))
|
||||
return
|
||||
}
|
||||
|
||||
if !u.CanInstall(version) {
|
||||
logrus.Info("A manual update is required")
|
||||
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.InstallUpdate(version); err != nil {
|
||||
if errors.Cause(err) == updater.ErrDownloadVerify {
|
||||
logrus.WithError(err).Warning("Skipping update installation due to temporary error")
|
||||
} else {
|
||||
logrus.WithError(err).Error("The update couldn't be installed")
|
||||
f.NotifySilentUpdateError(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f.NotifySilentUpdateInstalled()
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -19,12 +19,16 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
@ -37,8 +41,11 @@ var (
|
||||
type Bridge struct {
|
||||
*users.Users
|
||||
|
||||
pref PreferenceProvider
|
||||
locations Locator
|
||||
settings SettingsProvider
|
||||
clientManager users.ClientManager
|
||||
updater Updater
|
||||
versioner Versioner
|
||||
|
||||
userAgentClientName string
|
||||
userAgentClientVersion string
|
||||
@ -46,30 +53,40 @@ type Bridge struct {
|
||||
}
|
||||
|
||||
func New(
|
||||
config Configer,
|
||||
pref PreferenceProvider,
|
||||
locations Locator,
|
||||
cache Cacher,
|
||||
s SettingsProvider,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
clientManager users.ClientManager,
|
||||
credStorer users.CredentialsStorer,
|
||||
updater Updater,
|
||||
versioner Versioner,
|
||||
) *Bridge {
|
||||
// Allow DoH before starting the app if the user has previously set this setting.
|
||||
// This allows us to start even if protonmail is blocked.
|
||||
if pref.GetBool(preferences.AllowProxyKey) {
|
||||
if s.GetBool(settings.AllowProxyKey) {
|
||||
clientManager.AllowProxy()
|
||||
}
|
||||
|
||||
storeFactory := newStoreFactory(config, panicHandler, clientManager, eventListener)
|
||||
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory)
|
||||
storeFactory := newStoreFactory(cache, panicHandler, clientManager, eventListener)
|
||||
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
|
||||
b := &Bridge{
|
||||
Users: u,
|
||||
|
||||
pref: pref,
|
||||
locations: locations,
|
||||
settings: s,
|
||||
clientManager: clientManager,
|
||||
updater: updater,
|
||||
versioner: versioner,
|
||||
}
|
||||
|
||||
if pref.GetBool(preferences.FirstStartKey) {
|
||||
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
|
||||
if s.GetBool(settings.FirstStartKey) {
|
||||
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send metric")
|
||||
}
|
||||
|
||||
s.SetBool(settings.FirstStartKey, false)
|
||||
}
|
||||
|
||||
go b.heartbeat()
|
||||
@ -79,19 +96,25 @@ func New(
|
||||
|
||||
// heartbeat sends a heartbeat signal once a day.
|
||||
func (b *Bridge) heartbeat() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
|
||||
for range ticker.C {
|
||||
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
|
||||
for range time.Tick(time.Minute) {
|
||||
lastHeartbeatDay, err := strconv.ParseInt(b.settings.Get(settings.LastHeartbeatKey), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
nextTime := time.Unix(next, 0)
|
||||
if time.Now().After(nextTime) {
|
||||
b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
|
||||
nextTime = nextTime.Add(24 * time.Hour)
|
||||
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
|
||||
|
||||
// If we're still on the same day, don't send a heartbeat.
|
||||
if time.Now().YearDay() == int(lastHeartbeatDay) {
|
||||
continue
|
||||
}
|
||||
|
||||
// We're on the next (or a different) day, so send a heartbeat.
|
||||
if err := b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to send heartbeat")
|
||||
continue
|
||||
}
|
||||
|
||||
// Heartbeat was sent successfully so update the last heartbeat day.
|
||||
b.settings.Set(settings.LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,6 +143,12 @@ func (b *Bridge) SetCurrentOS(os string) {
|
||||
}
|
||||
|
||||
func (b *Bridge) updateUserAgent() {
|
||||
logrus.
|
||||
WithField("clientName", b.userAgentClientName).
|
||||
WithField("clientVersion", b.userAgentClientVersion).
|
||||
WithField("OS", b.userAgentOS).
|
||||
Info("Updating user agent")
|
||||
|
||||
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
|
||||
}
|
||||
|
||||
@ -129,15 +158,17 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
|
||||
defer c.Logout()
|
||||
|
||||
title := "[Bridge] Bug"
|
||||
if err := c.ReportBugWithEmailClient(
|
||||
osType,
|
||||
osVersion,
|
||||
title,
|
||||
description,
|
||||
accountName,
|
||||
address,
|
||||
emailClient,
|
||||
); err != nil {
|
||||
report := pmapi.ReportReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
Browser: emailClient,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Username: accountName,
|
||||
Email: address,
|
||||
}
|
||||
|
||||
if err := c.Report(report); err != nil {
|
||||
log.Error("Reporting bug failed: ", err)
|
||||
return err
|
||||
}
|
||||
@ -146,3 +177,36 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUpdateChannel returns currently set update channel.
|
||||
func (b *Bridge) GetUpdateChannel() updater.UpdateChannel {
|
||||
return updater.UpdateChannel(b.settings.Get(settings.UpdateChannelKey))
|
||||
}
|
||||
|
||||
// SetUpdateChannel switches update channel.
|
||||
// Downgrading to previous version (by switching from early to stable, for example)
|
||||
// requires clearing all data including update files due to possibility of
|
||||
// inconsistency between versions and absence of backwards migration scripts.
|
||||
func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) error {
|
||||
b.settings.Set(settings.UpdateChannelKey, string(channel))
|
||||
|
||||
version, err := b.updater.Check()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.updater.IsDowngrade(version) {
|
||||
if err := b.Users.ClearData(); err != nil {
|
||||
log.WithError(err).Error("Failed to clear data while downgrading channel")
|
||||
}
|
||||
if err := b.locations.ClearUpdates(); err != nil {
|
||||
log.WithError(err).Error("Failed to clear updates while downgrading channel")
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.updater.InstallUpdate(version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.versioner.RemoveOtherVersions(version.Version)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at Wed 29 Jul 2020 06:44:08 AM CEST. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Mon Feb 1 10:34:22 CET 2021. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;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-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/sentry-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/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;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/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -15,17 +15,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./release-notes.sh at Wed 29 Jul 2020 07:07:28 AM CEST. DO NOT EDIT.
|
||||
// Code generated by ./release-notes.sh at 'Fri Jan 22 11:01:06 AM CET 2021'. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const ReleaseNotes = `• Improvements to Alternative Routing: Version two of this feature is now more resilient to unstable internet connections, which results in a smoother experience using this feature. Also includes fixes to previous implementation of Alternative Routing when first starting the application or when turning it off.
|
||||
• Email parsing improvements: Improved detection of email encodings embedded in html/xml in addition to message header; add a fallback option if encoding is not specified and decoding as UTF8 fails (ISO-8859-1) ; tweaked logic of parsing "References" header.
|
||||
• User interaction improvements: Some smaller improvements in specific cases to make the interaction with Proton Bridge clearer for the user
|
||||
• Code updates & maintenance: Migrated to GopenPGP v2, updates to GoIMAPv1, increased bbolt version to 1.3.5 and various improvements regarding extensibility and maintainability for upcoming work.
|
||||
• General stability improvements: Improvements to the behavior of the application under various unstable internet conditions.
|
||||
const ReleaseNotes = `
|
||||
`
|
||||
|
||||
const ReleaseFixedBugs = `• Fixed a slew of smaller bugs and some conditions which could cause the application to crash.
|
||||
• The full changelog can be found at https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
|
||||
const ReleaseFixedBugs = `• Fixed sending error caused by inconsistent use of upper and lower case in sender’s email address
|
||||
`
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
type storeFactory struct {
|
||||
config StoreFactoryConfiger
|
||||
cache Cacher
|
||||
panicHandler users.PanicHandler
|
||||
clientManager users.ClientManager
|
||||
eventListener listener.Listener
|
||||
@ -36,29 +36,29 @@ type storeFactory struct {
|
||||
}
|
||||
|
||||
func newStoreFactory(
|
||||
config StoreFactoryConfiger,
|
||||
cache Cacher,
|
||||
panicHandler users.PanicHandler,
|
||||
clientManager users.ClientManager,
|
||||
eventListener listener.Listener,
|
||||
) *storeFactory {
|
||||
return &storeFactory{
|
||||
config: config,
|
||||
cache: cache,
|
||||
panicHandler: panicHandler,
|
||||
clientManager: clientManager,
|
||||
eventListener: eventListener,
|
||||
storeCache: store.NewCache(config.GetIMAPCachePath()),
|
||||
storeCache: store.NewCache(cache.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())
|
||||
storePath := getUserStorePath(f.cache.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)
|
||||
storePath := getUserStorePath(f.cache.GetDBDir(), userID)
|
||||
return store.RemoveStore(f.storeCache, storePath, userID)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -17,21 +17,35 @@
|
||||
|
||||
package bridge
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/internal/users"
|
||||
import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
|
||||
type Configer interface {
|
||||
users.Configer
|
||||
StoreFactoryConfiger
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
)
|
||||
|
||||
type Locator interface {
|
||||
Clear() error
|
||||
ClearUpdates() error
|
||||
}
|
||||
|
||||
type StoreFactoryConfiger interface {
|
||||
GetDBDir() string
|
||||
type Cacher interface {
|
||||
GetIMAPCachePath() string
|
||||
GetDBDir() string
|
||||
}
|
||||
|
||||
type PreferenceProvider interface {
|
||||
type SettingsProvider interface {
|
||||
Get(key string) string
|
||||
GetBool(key string) bool
|
||||
GetInt(key string) int
|
||||
Set(key string, value string)
|
||||
GetBool(key string) bool
|
||||
SetBool(key string, val bool)
|
||||
}
|
||||
|
||||
type Updater interface {
|
||||
Check() (updater.VersionInfo, error)
|
||||
IsDowngrade(updater.VersionInfo) bool
|
||||
InstallUpdate(updater.VersionInfo) error
|
||||
}
|
||||
|
||||
type Versioner interface {
|
||||
RemoveOtherVersions(*semver.Version) error
|
||||
}
|
||||
|
||||
65
internal/config/cache/cache.go
vendored
Normal file
65
internal/config/cache/cache.go
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2021 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 cache provides access to contents inside a cache directory.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/files"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
dir, version string
|
||||
}
|
||||
|
||||
func New(dir, version string) (*Cache, error) {
|
||||
if err := os.MkdirAll(filepath.Join(dir, version), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Cache{
|
||||
dir: dir,
|
||||
version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDBDir returns folder for db files.
|
||||
func (c *Cache) GetDBDir() string {
|
||||
return c.getCurrentCacheDir()
|
||||
}
|
||||
|
||||
// GetIMAPCachePath returns path to file with IMAP status.
|
||||
func (c *Cache) GetIMAPCachePath() string {
|
||||
return filepath.Join(c.getCurrentCacheDir(), "user_info.json")
|
||||
}
|
||||
|
||||
// GetTransferDir returns folder for import-export rules files.
|
||||
func (c *Cache) GetTransferDir() string {
|
||||
return c.getCurrentCacheDir()
|
||||
}
|
||||
|
||||
// RemoveOldVersions removes any cache dirs that are not the current version.
|
||||
func (c *Cache) RemoveOldVersions() error {
|
||||
return files.Remove(c.dir).Except(c.getCurrentCacheDir()).Do()
|
||||
}
|
||||
|
||||
func (c *Cache) getCurrentCacheDir() string {
|
||||
return filepath.Join(c.dir, c.version)
|
||||
}
|
||||
70
internal/config/cache/cache_test.go
vendored
Normal file
70
internal/config/cache/cache_test.go
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2021 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 cache
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRemoveOldVersions(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-cache")
|
||||
require.NoError(t, err)
|
||||
|
||||
cache, err := New(dir, "c4")
|
||||
require.NoError(t, err)
|
||||
|
||||
createFilesInDir(t, dir,
|
||||
"unexpected1.txt",
|
||||
"c1/unexpected1.txt",
|
||||
"c2/unexpected2.txt",
|
||||
"c3/unexpected3.txt",
|
||||
"something.txt",
|
||||
)
|
||||
|
||||
require.DirExists(t, filepath.Join(dir, "c4"))
|
||||
require.FileExists(t, filepath.Join(dir, "unexpected1.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
|
||||
require.FileExists(t, filepath.Join(dir, "something.txt"))
|
||||
|
||||
assert.NoError(t, cache.RemoveOldVersions())
|
||||
|
||||
assert.DirExists(t, filepath.Join(dir, "c4"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "unexpected1.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
|
||||
assert.NoFileExists(t, filepath.Join(dir, "something.txt"))
|
||||
}
|
||||
|
||||
func createFilesInDir(t *testing.T, dir string, files ...string) {
|
||||
for _, target := range files {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, target)), 0700))
|
||||
|
||||
f, err := os.Create(filepath.Join(dir, target))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -15,35 +15,38 @@
|
||||
// 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 config
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Preferences struct {
|
||||
type keyValueStore struct {
|
||||
cache map[string]string
|
||||
path string
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPreferences returns loaded preferences.
|
||||
func NewPreferences(preferencesPath string) *Preferences {
|
||||
p := &Preferences{
|
||||
path: preferencesPath,
|
||||
// newKeyValueStore returns loaded preferences.
|
||||
func newKeyValueStore(path string) *keyValueStore {
|
||||
p := &keyValueStore{
|
||||
path: path,
|
||||
lock: &sync.RWMutex{},
|
||||
}
|
||||
if err := p.load(); err != nil {
|
||||
log.Warn("Cannot load preferences: ", err)
|
||||
logrus.WithError(err).Warn("Cannot load preferences file, creating new one")
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Preferences) load() error {
|
||||
func (p *keyValueStore) load() error {
|
||||
if p.cache != nil {
|
||||
return nil
|
||||
}
|
||||
@ -62,7 +65,7 @@ func (p *Preferences) load() error {
|
||||
return json.NewDecoder(f).Decode(&p.cache)
|
||||
}
|
||||
|
||||
func (p *Preferences) save() error {
|
||||
func (p *keyValueStore) save() error {
|
||||
if p.cache == nil {
|
||||
return errors.New("cannot save preferences: cache is nil")
|
||||
}
|
||||
@ -79,42 +82,50 @@ func (p *Preferences) save() error {
|
||||
return json.NewEncoder(f).Encode(p.cache)
|
||||
}
|
||||
|
||||
func (p *Preferences) SetDefault(key, value string) {
|
||||
func (p *keyValueStore) setDefault(key, value string) {
|
||||
if p.Get(key) == "" {
|
||||
p.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Preferences) Get(key string) string {
|
||||
func (p *keyValueStore) Get(key string) string {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
return p.cache[key]
|
||||
}
|
||||
|
||||
func (p *Preferences) GetBool(key string) bool {
|
||||
func (p *keyValueStore) GetBool(key string) bool {
|
||||
return p.Get(key) == "true"
|
||||
}
|
||||
|
||||
func (p *Preferences) GetInt(key string) int {
|
||||
func (p *keyValueStore) GetInt(key string) int {
|
||||
value, err := strconv.Atoi(p.Get(key))
|
||||
if err != nil {
|
||||
log.Error("Cannot parse int: ", err)
|
||||
logrus.WithError(err).Error("Cannot parse int")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *Preferences) Set(key, value string) {
|
||||
func (p *keyValueStore) GetFloat64(key string) float64 {
|
||||
value, err := strconv.ParseFloat(p.Get(key), 64)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Cannot parse float64")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *keyValueStore) Set(key, value string) {
|
||||
p.lock.Lock()
|
||||
p.cache[key] = value
|
||||
p.lock.Unlock()
|
||||
|
||||
if err := p.save(); err != nil {
|
||||
log.Warn("Cannot save preferences: ", err)
|
||||
logrus.WithError(err).Warn("Cannot save preferences")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Preferences) SetBool(key string, value bool) {
|
||||
func (p *keyValueStore) SetBool(key string, value bool) {
|
||||
if value {
|
||||
p.Set(key, "true")
|
||||
} else {
|
||||
@ -122,6 +133,10 @@ func (p *Preferences) SetBool(key string, value bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Preferences) SetInt(key string, value int) {
|
||||
func (p *keyValueStore) SetInt(key string, value int) {
|
||||
p.Set(key, strconv.Itoa(value))
|
||||
}
|
||||
|
||||
func (p *keyValueStore) SetFloat64(key string, value float64) {
|
||||
p.Set(key, fmt.Sprintf("%v", value))
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
package settings
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@ -27,82 +27,78 @@ import (
|
||||
|
||||
const testPrefFilePath = "/tmp/pref.json"
|
||||
|
||||
func shutdownTestPreferences() {
|
||||
_ = os.RemoveAll(testPrefFilePath)
|
||||
}
|
||||
|
||||
func TestLoadNoPreferences(t *testing.T) {
|
||||
pref := newTestEmptyPreferences(t)
|
||||
func TestLoadNoKeyValueStore(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
require.Equal(t, "", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestLoadBadPreferences(t *testing.T) {
|
||||
func TestLoadBadKeyValueStore(t *testing.T) {
|
||||
require.NoError(t, ioutil.WriteFile(testPrefFilePath, []byte("{\"key\":\"value"), 0700))
|
||||
pref := NewPreferences(testPrefFilePath)
|
||||
pref := newKeyValueStore(testPrefFilePath)
|
||||
require.Equal(t, "", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestPreferencesGet(t *testing.T) {
|
||||
pref := newTestPreferences(t)
|
||||
func TestKeyValueStoreGet(t *testing.T) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, "value", pref.Get("str"))
|
||||
require.Equal(t, "42", pref.Get("int"))
|
||||
require.Equal(t, "true", pref.Get("bool"))
|
||||
require.Equal(t, "t", pref.Get("falseBool"))
|
||||
}
|
||||
|
||||
func TestPreferencesGetInt(t *testing.T) {
|
||||
pref := newTestPreferences(t)
|
||||
func TestKeyValueStoreGetInt(t *testing.T) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, 0, pref.GetInt("str"))
|
||||
require.Equal(t, 42, pref.GetInt("int"))
|
||||
require.Equal(t, 0, pref.GetInt("bool"))
|
||||
require.Equal(t, 0, pref.GetInt("falseBool"))
|
||||
}
|
||||
|
||||
func TestPreferencesGetBool(t *testing.T) {
|
||||
pref := newTestPreferences(t)
|
||||
func TestKeyValueStoreGetBool(t *testing.T) {
|
||||
pref := newTestKeyValueStore(t)
|
||||
require.Equal(t, false, pref.GetBool("str"))
|
||||
require.Equal(t, false, pref.GetBool("int"))
|
||||
require.Equal(t, true, pref.GetBool("bool"))
|
||||
require.Equal(t, false, pref.GetBool("falseBool"))
|
||||
}
|
||||
|
||||
func TestPreferencesSetDefault(t *testing.T) {
|
||||
pref := newTestEmptyPreferences(t)
|
||||
pref.SetDefault("key", "value")
|
||||
pref.SetDefault("key", "othervalue")
|
||||
func TestKeyValueStoreSetDefault(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.setDefault("key", "value")
|
||||
pref.setDefault("key", "othervalue")
|
||||
require.Equal(t, "value", pref.Get("key"))
|
||||
}
|
||||
|
||||
func TestPreferencesSet(t *testing.T) {
|
||||
pref := newTestEmptyPreferences(t)
|
||||
func TestKeyValueStoreSet(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.Set("str", "value")
|
||||
checkSavedPreferences(t, "{\"str\":\"value\"}")
|
||||
checkSavedKeyValueStore(t, "{\"str\":\"value\"}")
|
||||
}
|
||||
|
||||
func TestPreferencesSetInt(t *testing.T) {
|
||||
pref := newTestEmptyPreferences(t)
|
||||
func TestKeyValueStoreSetInt(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.SetInt("int", 42)
|
||||
checkSavedPreferences(t, "{\"int\":\"42\"}")
|
||||
checkSavedKeyValueStore(t, "{\"int\":\"42\"}")
|
||||
}
|
||||
|
||||
func TestPreferencesSetBool(t *testing.T) {
|
||||
pref := newTestEmptyPreferences(t)
|
||||
func TestKeyValueStoreSetBool(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.SetBool("trueBool", true)
|
||||
pref.SetBool("falseBool", false)
|
||||
checkSavedPreferences(t, "{\"falseBool\":\"false\",\"trueBool\":\"true\"}")
|
||||
checkSavedKeyValueStore(t, "{\"falseBool\":\"false\",\"trueBool\":\"true\"}")
|
||||
}
|
||||
|
||||
func newTestEmptyPreferences(t *testing.T) *Preferences {
|
||||
func newTestEmptyKeyValueStore(t *testing.T) *keyValueStore {
|
||||
require.NoError(t, os.RemoveAll(testPrefFilePath))
|
||||
return NewPreferences(testPrefFilePath)
|
||||
return newKeyValueStore(testPrefFilePath)
|
||||
}
|
||||
|
||||
func newTestPreferences(t *testing.T) *Preferences {
|
||||
func newTestKeyValueStore(t *testing.T) *keyValueStore {
|
||||
require.NoError(t, ioutil.WriteFile(testPrefFilePath, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0700))
|
||||
return NewPreferences(testPrefFilePath)
|
||||
return newKeyValueStore(testPrefFilePath)
|
||||
}
|
||||
|
||||
func checkSavedPreferences(t *testing.T, expected string) {
|
||||
func checkSavedKeyValueStore(t *testing.T, expected string) {
|
||||
data, err := ioutil.ReadFile(testPrefFilePath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected+"\n", string(data))
|
||||
90
internal/config/settings/settings.go
Normal file
90
internal/config/settings/settings.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2021 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 settings provides access to persistent user settings.
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Keys of preferences in JSON file.
|
||||
const (
|
||||
FirstStartKey = "first_time_start"
|
||||
FirstStartGUIKey = "first_time_start_gui"
|
||||
LastHeartbeatKey = "last_heartbeat"
|
||||
APIPortKey = "user_port_api"
|
||||
IMAPPortKey = "user_port_imap"
|
||||
SMTPPortKey = "user_port_smtp"
|
||||
SMTPSSLKey = "user_ssl_smtp"
|
||||
AllowProxyKey = "allow_proxy"
|
||||
AutostartKey = "autostart"
|
||||
AutoUpdateKey = "autoupdate"
|
||||
CookiesKey = "cookies"
|
||||
ReportOutgoingNoEncKey = "report_outgoing_email_without_encryption"
|
||||
LastVersionKey = "last_used_version"
|
||||
UpdateChannelKey = "update_channel"
|
||||
RolloutKey = "rollout"
|
||||
PreferredKeychainKey = "preferred_keychain"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
*keyValueStore
|
||||
|
||||
settingsPath string
|
||||
}
|
||||
|
||||
func New(settingsPath string) *Settings {
|
||||
s := &Settings{
|
||||
keyValueStore: newKeyValueStore(filepath.Join(settingsPath, "prefs.json")),
|
||||
settingsPath: settingsPath,
|
||||
}
|
||||
|
||||
s.setDefaultValues()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultIMAPPort = "1143"
|
||||
DefaultSMTPPort = "1025"
|
||||
DefaultAPIPort = "1042"
|
||||
)
|
||||
|
||||
func (s *Settings) setDefaultValues() {
|
||||
s.setDefault(FirstStartKey, "true")
|
||||
s.setDefault(FirstStartGUIKey, "true")
|
||||
s.setDefault(LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
|
||||
s.setDefault(AllowProxyKey, "true")
|
||||
s.setDefault(AutostartKey, "true")
|
||||
s.setDefault(AutoUpdateKey, "true")
|
||||
s.setDefault(ReportOutgoingNoEncKey, "false")
|
||||
s.setDefault(LastVersionKey, "")
|
||||
s.setDefault(UpdateChannelKey, "")
|
||||
s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64()))
|
||||
s.setDefault(PreferredKeychainKey, "")
|
||||
|
||||
s.setDefault(APIPortKey, DefaultAPIPort)
|
||||
s.setDefault(IMAPPortKey, DefaultIMAPPort)
|
||||
s.setDefault(SMTPPortKey, DefaultSMTPPort)
|
||||
|
||||
// By default, stick to STARTTLS. If the user uses catalina+applemail they'll have to change to SSL.
|
||||
s.setDefault(SMTPSSLKey, "false")
|
||||
}
|
||||
53
internal/config/tls/cert_store_darwin.go
Normal file
53
internal/config/tls/cert_store_darwin.go
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2021 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 tls
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func addTrustedCert(certPath string) error {
|
||||
return exec.Command( // nolint[gosec]
|
||||
"/usr/bin/security",
|
||||
"execute-with-privileges",
|
||||
"/usr/bin/security",
|
||||
"add-trusted-cert",
|
||||
"-d",
|
||||
"-r", "trustRoot",
|
||||
"-p", "ssl",
|
||||
"-k", "/Library/Keychains/System.keychain",
|
||||
certPath,
|
||||
).Run()
|
||||
}
|
||||
|
||||
func removeTrustedCert(certPath string) error {
|
||||
return exec.Command( // nolint[gosec]
|
||||
"/usr/bin/security",
|
||||
"execute-with-privileges",
|
||||
"/usr/bin/security",
|
||||
"remove-trusted-cert",
|
||||
"-d",
|
||||
certPath,
|
||||
).Run()
|
||||
}
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
return addTrustedCert(t.getTLSCertPath())
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
return removeTrustedCert(t.getTLSCertPath())
|
||||
}
|
||||
26
internal/config/tls/cert_store_linux.go
Normal file
26
internal/config/tls/cert_store_linux.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2021 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 tls
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
26
internal/config/tls/cert_store_windows.go
Normal file
26
internal/config/tls/cert_store_windows.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2021 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 tls
|
||||
|
||||
func (t *TLS) InstallCerts() error {
|
||||
return nil // NOTE(GODT-986): Install certs to root cert store?
|
||||
}
|
||||
|
||||
func (t *TLS) UninstallCerts() error {
|
||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||
}
|
||||
158
internal/config/tls/tls.go
Normal file
158
internal/config/tls/tls.go
Normal file
@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2021 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 tls
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TLS struct {
|
||||
settingsPath string
|
||||
}
|
||||
|
||||
func New(settingsPath string) *TLS {
|
||||
return &TLS{
|
||||
settingsPath: settingsPath,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTLSTemplate creates a new TLS template certificate with a random serial number.
|
||||
func NewTLSTemplate() (*x509.Certificate, error) {
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate serial number")
|
||||
}
|
||||
|
||||
return &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"CH"},
|
||||
Organization: []string{"Proton Technologies AG"},
|
||||
OrganizationalUnit: []string{"ProtonMail"},
|
||||
CommonName: "127.0.0.1",
|
||||
},
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(20 * 365 * 24 * time.Hour),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon")
|
||||
|
||||
// getTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP).
|
||||
func (t *TLS) getTLSCertPath() string {
|
||||
return filepath.Join(t.settingsPath, "cert.pem")
|
||||
}
|
||||
|
||||
// getTLSKeyPath returns path to private key; used for TLS servers (IMAP, SMTP).
|
||||
func (t *TLS) getTLSKeyPath() string {
|
||||
return filepath.Join(t.settingsPath, "key.pem")
|
||||
}
|
||||
|
||||
// HasCerts returns whether TLS certs have been generated.
|
||||
func (t *TLS) HasCerts() bool {
|
||||
if _, err := os.Stat(t.getTLSCertPath()); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := os.Stat(t.getTLSKeyPath()); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GenerateCerts generates certs from the given template.
|
||||
func (t *TLS) GenerateCerts(template *x509.Certificate) error {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate private key")
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create certificate")
|
||||
}
|
||||
|
||||
certOut, err := os.Create(t.getTLSCertPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer certOut.Close() // nolint[errcheck]
|
||||
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyOut, err := os.OpenFile(t.getTLSKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close() // nolint[errcheck]
|
||||
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig tries to load TLS config or generate new one which is then returned.
|
||||
func (t *TLS) GetConfig() (*tls.Config, error) {
|
||||
c, err := tls.LoadX509KeyPair(t.getTLSCertPath(), t.getTLSKeyPath())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load keypair")
|
||||
}
|
||||
|
||||
c.Leaf, err = x509.ParseCertificate(c.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse certificate")
|
||||
}
|
||||
|
||||
if time.Now().Add(31 * 24 * time.Hour).After(c.Leaf.NotAfter) {
|
||||
return nil, ErrTLSCertExpiresSoon
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(c.Leaf)
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{c},
|
||||
ServerName: "127.0.0.1",
|
||||
ClientAuth: tls.VerifyClientCertIfGiven,
|
||||
RootCAs: caCertPool,
|
||||
ClientCAs: caCertPool,
|
||||
}, nil
|
||||
}
|
||||
77
internal/config/tls/tls_test.go
Normal file
77
internal/config/tls/tls_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2021 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 tls
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetOldConfig(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-tls")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new tls object.
|
||||
tls := New(dir)
|
||||
|
||||
// Create new TLS template.
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make the template be an old key.
|
||||
tlsTemplate.NotBefore = time.Now().Add(-365 * 24 * time.Hour)
|
||||
tlsTemplate.NotAfter = time.Now()
|
||||
|
||||
// Generate the certs from the template.
|
||||
require.NoError(t, tls.GenerateCerts(tlsTemplate))
|
||||
|
||||
// Generate the config from the certs -- it's going to expire soon so we don't want to use it.
|
||||
_, err = tls.GetConfig()
|
||||
require.Equal(t, err, ErrTLSCertExpiresSoon)
|
||||
}
|
||||
|
||||
func TestGetValidConfig(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "test-tls")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new tls object.
|
||||
tls := New(dir)
|
||||
|
||||
// Create new TLS template.
|
||||
tlsTemplate, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make the template be a new key.
|
||||
tlsTemplate.NotBefore = time.Now()
|
||||
tlsTemplate.NotAfter = time.Now().Add(2 * 365 * 24 * time.Hour)
|
||||
|
||||
// Generate the certs from the template.
|
||||
require.NoError(t, tls.GenerateCerts(tlsTemplate))
|
||||
|
||||
// Generate the config from the certs -- it's not going to expire soon so we want to use it.
|
||||
config, err := tls.GetConfig()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(config.Certificates), 1)
|
||||
|
||||
// Check the cert is valid.
|
||||
now, notValidAfter := time.Now(), config.Certificates[0].Leaf.NotAfter
|
||||
require.False(t, now.After(notValidAfter), "new certificate expected to be valid at %v but have valid until %v", now, notValidAfter)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.Bridge.
|
||||
//
|
||||
@ -18,6 +18,10 @@
|
||||
// Package constants contains variables that are set via ldflags during build.
|
||||
package constants
|
||||
|
||||
import "fmt"
|
||||
|
||||
const VendorName = "protonmail"
|
||||
|
||||
// nolint[gochecknoglobals]
|
||||
var (
|
||||
// Version of the build.
|
||||
@ -29,15 +33,9 @@ var (
|
||||
// BuildTime stamp of the build.
|
||||
BuildTime = ""
|
||||
|
||||
// AppShortName to make setup.
|
||||
AppShortName = "bridge"
|
||||
|
||||
// DSNSentry client keys to be able to report crashes to Sentry.
|
||||
DSNSentry = ""
|
||||
|
||||
// LongVersion is derived from Version and Revision.
|
||||
LongVersion = Version + " (" + Revision + ")"
|
||||
|
||||
// BuildVersion is derived from LongVersion and BuildTime.
|
||||
BuildVersion = LongVersion + " " + BuildTime
|
||||
BuildVersion = fmt.Sprintf("%v (%v) %v", Version, Revision, BuildTime)
|
||||
)
|
||||
143
internal/cookies/jar.go
Normal file
143
internal/cookies/jar.go
Normal file
@ -0,0 +1,143 @@
|
||||
// Copyright (c) 2021 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
)
|
||||
|
||||
type cookiesByHost map[string][]*http.Cookie
|
||||
|
||||
// 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
|
||||
settings *settings.Settings
|
||||
cookies cookiesByHost
|
||||
locker sync.Locker
|
||||
}
|
||||
|
||||
func NewCookieJar(s *settings.Settings) (*Jar, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cookiesByHost, err := loadCookies(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for host, cookies := range cookiesByHost {
|
||||
url, err := url.Parse(host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
jar.SetCookies(url, cookies)
|
||||
}
|
||||
|
||||
return &Jar{
|
||||
jar: jar,
|
||||
settings: s,
|
||||
cookies: cookiesByHost,
|
||||
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)
|
||||
|
||||
for _, cookie := range cookies {
|
||||
if cookie.MaxAge > 0 {
|
||||
cookie.Expires = time.Now().Add(time.Duration(cookie.MaxAge) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
j.cookies[fmt.Sprintf("%v://%v", u.Scheme, u.Host)] = cookies
|
||||
}
|
||||
|
||||
func (j *Jar) Cookies(u *url.URL) []*http.Cookie {
|
||||
j.locker.Lock()
|
||||
defer j.locker.Unlock()
|
||||
|
||||
return j.jar.Cookies(u)
|
||||
}
|
||||
|
||||
// PersistCookies persists the cookies to disk.
|
||||
func (j *Jar) PersistCookies() error {
|
||||
j.locker.Lock()
|
||||
defer j.locker.Unlock()
|
||||
|
||||
rawCookies, err := json.Marshal(j.cookies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
j.settings.Set(settings.CookiesKey, string(rawCookies))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadCookies loads all non-expired cookies from disk.
|
||||
func loadCookies(s *settings.Settings) (cookiesByHost, error) {
|
||||
rawCookies := s.Get(settings.CookiesKey)
|
||||
|
||||
if rawCookies == "" {
|
||||
return make(cookiesByHost), nil
|
||||
}
|
||||
|
||||
var cookiesByHost cookiesByHost
|
||||
|
||||
if err := json.Unmarshal([]byte(rawCookies), &cookiesByHost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for host, cookies := range cookiesByHost {
|
||||
if validCookies := discardExpiredCookies(cookies); len(validCookies) > 0 {
|
||||
cookiesByHost[host] = validCookies
|
||||
}
|
||||
}
|
||||
|
||||
return cookiesByHost, nil
|
||||
}
|
||||
|
||||
// discardExpiredCookies returns all the given cookies which aren't expired.
|
||||
func discardExpiredCookies(cookies []*http.Cookie) []*http.Cookie {
|
||||
var validCookies []*http.Cookie
|
||||
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Expires.After(time.Now()) {
|
||||
validCookies = append(validCookies, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
return validCookies
|
||||
}
|
||||
179
internal/cookies/jar_test.go
Normal file
179
internal/cookies/jar_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2021 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 (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"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, newFakeSettings())
|
||||
|
||||
// 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.
|
||||
s := newFakeSettings()
|
||||
|
||||
// This client saves cookies to persistent storage.
|
||||
oldClient, jar := getClientWithJar(t, s)
|
||||
|
||||
// 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())
|
||||
|
||||
// Save the cookies.
|
||||
require.NoError(t, jar.PersistCookies())
|
||||
|
||||
// This client loads cookies from persistent storage.
|
||||
newClient, _ := getClientWithJar(t, s)
|
||||
|
||||
// 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.
|
||||
s := newFakeSettings()
|
||||
|
||||
// This client saves cookies to persistent storage.
|
||||
oldClient, jar1 := getClientWithJar(t, s)
|
||||
|
||||
// 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())
|
||||
|
||||
// Save the cookies.
|
||||
require.NoError(t, jar1.PersistCookies())
|
||||
|
||||
// Wait until the second cookie expires.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Load a client, which will clear out expired cookies.
|
||||
_, jar2 := getClientWithJar(t, s)
|
||||
|
||||
// Save the cookies (expired ones were cleared out).
|
||||
require.NoError(t, jar2.PersistCookies())
|
||||
|
||||
assert.Contains(t, s.Get(settings.CookiesKey), "TestName1")
|
||||
assert.NotContains(t, s.Get(settings.CookiesKey), "TestName2")
|
||||
assert.Contains(t, s.Get(settings.CookiesKey), "TestName3")
|
||||
}
|
||||
|
||||
type testCookie struct {
|
||||
name, value string
|
||||
maxAge int
|
||||
}
|
||||
|
||||
func getClientWithJar(t *testing.T, s *settings.Settings) (*http.Client, *Jar) {
|
||||
jar, err := NewCookieJar(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &http.Client{Jar: 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)
|
||||
}
|
||||
|
||||
// newFakeSettings creates a temporary folder for files.
|
||||
func newFakeSettings() *settings.Settings {
|
||||
dir, err := ioutil.TempDir("", "test-settings")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return settings.New(dir)
|
||||
}
|
||||
42
internal/crash/actions.go
Normal file
42
internal/crash/actions.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2021 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 crash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/0xAX/notificator"
|
||||
)
|
||||
|
||||
// ShowErrorNotification shows a system notification that the app with the given appName has crashed.
|
||||
// NOTE: Icons shouldn't be hardcoded.
|
||||
func ShowErrorNotification(appName string) RecoveryAction {
|
||||
return func(r interface{}) error {
|
||||
notify := notificator.New(notificator.Options{
|
||||
DefaultIcon: "../frontend/ui/icon/icon.png",
|
||||
AppName: appName,
|
||||
})
|
||||
|
||||
return notify.Push(
|
||||
"Fatal Error",
|
||||
fmt.Sprintf("%v has encountered a fatal error.", appName),
|
||||
"/frontend/icon/icon.png",
|
||||
notificator.UR_CRITICAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
54
internal/crash/handler.go
Normal file
54
internal/crash/handler.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2021 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 crash implements a crash handler with configurable recovery actions.
|
||||
package crash
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type RecoveryAction func(interface{}) error
|
||||
|
||||
type Handler struct {
|
||||
actions []RecoveryAction
|
||||
}
|
||||
|
||||
func NewHandler(actions ...RecoveryAction) *Handler {
|
||||
return &Handler{actions: actions}
|
||||
}
|
||||
|
||||
func (h *Handler) AddRecoveryAction(action RecoveryAction) *Handler {
|
||||
h.actions = append(h.actions, action)
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) HandlePanic() {
|
||||
sentry.SkipDuringUnwind()
|
||||
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, action := range h.actions {
|
||||
if err := action(r); err != nil {
|
||||
logrus.WithError(err).Error("Failed to execute recovery action")
|
||||
}
|
||||
}
|
||||
}
|
||||
58
internal/crash/handler_test.go
Normal file
58
internal/crash/handler_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2021 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 crash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
var s string
|
||||
|
||||
h := NewHandler(
|
||||
func(r interface{}) error {
|
||||
s += fmt.Sprintf("1: %v\n", r)
|
||||
return nil
|
||||
},
|
||||
func(r interface{}) error {
|
||||
s += fmt.Sprintf("2: %v\n", r)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
h.
|
||||
AddRecoveryAction(func(r interface{}) error {
|
||||
s += fmt.Sprintf("3: %v\n", r)
|
||||
return nil
|
||||
}).
|
||||
AddRecoveryAction(func(r interface{}) error {
|
||||
s += fmt.Sprintf("4: %v\n", r)
|
||||
return nil
|
||||
})
|
||||
|
||||
defer func() {
|
||||
assert.Equal(t, "1: thing\n2: thing\n3: thing\n4: thing\n", s)
|
||||
}()
|
||||
|
||||
defer h.HandlePanic()
|
||||
|
||||
panic("thing")
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -27,6 +27,7 @@ import (
|
||||
// Constants of events used by the event listener in bridge.
|
||||
const (
|
||||
ErrorEvent = "error"
|
||||
CredentialsErrorEvent = "credentialsError"
|
||||
CloseConnectionEvent = "closeConnection"
|
||||
LogoutEvent = "logout"
|
||||
AddressChangedEvent = "addressChanged"
|
||||
@ -40,7 +41,6 @@ const (
|
||||
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
|
||||
UpgradeApplicationEvent = "upgradeApplication"
|
||||
TLSCertIssue = "tlsCertPinningIssue"
|
||||
IMAPTLSBadCert = "imapTLSBadCert"
|
||||
|
||||
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
|
||||
LogoutEventTimeout = 3 * time.Minute
|
||||
@ -49,6 +49,9 @@ const (
|
||||
// SetupEvents specific to event type and data.
|
||||
func SetupEvents(listener listener.Listener) {
|
||||
listener.SetLimit(LogoutEvent, LogoutEventTimeout)
|
||||
listener.SetBuffer(TLSCertIssue)
|
||||
listener.SetBuffer(ErrorEvent)
|
||||
listener.SetBuffer(CredentialsErrorEvent)
|
||||
listener.SetBuffer(InternetOffEvent)
|
||||
listener.SetBuffer(UpgradeApplicationEvent)
|
||||
listener.SetBuffer(TLSCertIssue)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -28,9 +28,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mobileconfig "github.com/ProtonMail/go-apple-mobileconfig"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/mobileconfig"
|
||||
)
|
||||
|
||||
func init() { //nolint[gochecknoinit]
|
||||
@ -43,7 +43,7 @@ func (c *appleMail) Name() string {
|
||||
return "Apple Mail"
|
||||
}
|
||||
|
||||
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.BridgeUser, addressIndex int) error { //nolint[funlen]
|
||||
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) error { //nolint[funlen]
|
||||
var addresses string
|
||||
var displayName string
|
||||
|
||||
@ -66,17 +66,17 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
|
||||
EmailAddress: addresses,
|
||||
DisplayName: displayName,
|
||||
Identifier: "protonmail " + displayName + timestamp,
|
||||
Imap: &mobileconfig.Imap{
|
||||
IMAP: &mobileconfig.IMAP{
|
||||
Hostname: bridge.Host,
|
||||
Port: imapPort,
|
||||
Tls: imapSSL,
|
||||
TLS: imapSSL,
|
||||
Username: displayName,
|
||||
Password: user.GetBridgePassword(),
|
||||
},
|
||||
Smtp: &mobileconfig.Smtp{
|
||||
SMTP: &mobileconfig.SMTP{
|
||||
Hostname: bridge.Host,
|
||||
Port: smtpPort,
|
||||
Tls: smtpSSL,
|
||||
TLS: smtpSSL,
|
||||
Username: displayName,
|
||||
},
|
||||
}
|
||||
@ -98,7 +98,7 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
|
||||
return err
|
||||
}
|
||||
|
||||
if err := mc.WriteTo(f); err != nil {
|
||||
if err := mc.WriteOut(f); err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -23,7 +23,7 @@ import "github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
|
||||
type AutoConfig interface {
|
||||
Name() string
|
||||
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.BridgeUser, addressIndex int) error
|
||||
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.User, addressIndex int) error
|
||||
}
|
||||
|
||||
var available []AutoConfig //nolint[gochecknoglobals]
|
||||
|
||||
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) 2021 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) 2021 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")
|
||||
}
|
||||
236
internal/frontend/cli-ie/frontend.go
Normal file
236
internal/frontend/cli-ie/frontend.go
Normal file
@ -0,0 +1,236 @@
|
||||
// Copyright (c) 2021 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/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"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
|
||||
|
||||
locations *locations.Locations
|
||||
eventListener listener.Listener
|
||||
updater types.Updater
|
||||
ie types.ImportExporter
|
||||
|
||||
restarter types.Restarter
|
||||
}
|
||||
|
||||
// New returns a new CLI frontend configured with the given options.
|
||||
func New( //nolint[funlen]
|
||||
panicHandler types.PanicHandler,
|
||||
|
||||
locations *locations.Locations,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
ie types.ImportExporter,
|
||||
restarter types.Restarter,
|
||||
) *frontendCLI { //nolint[golint]
|
||||
fe := &frontendCLI{
|
||||
Shell: ishell.New(),
|
||||
|
||||
locations: locations,
|
||||
eventListener: eventListener,
|
||||
updater: updater,
|
||||
ie: ie,
|
||||
|
||||
restarter: restarter,
|
||||
}
|
||||
|
||||
// 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: "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()
|
||||
}()
|
||||
return fe
|
||||
}
|
||||
|
||||
func (f *frontendCLI) watchEvents() {
|
||||
errorCh := f.getEventChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
|
||||
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 <-credentialsErrorCh:
|
||||
f.notifyCredentialsError()
|
||||
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)
|
||||
f.eventListener.RetryEmit(event)
|
||||
return ch
|
||||
}
|
||||
|
||||
// Loop starts the frontend loop with an interactive shell.
|
||||
func (f *frontendCLI) Loop() error {
|
||||
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
|
||||
}
|
||||
|
||||
func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
|
||||
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
|
||||
}
|
||||
|
||||
func (f *frontendCLI) WaitUntilFrontendIsReady() {}
|
||||
func (f *frontendCLI) SetVersion(version updater.VersionInfo) {}
|
||||
func (f *frontendCLI) NotifySilentUpdateInstalled() {}
|
||||
func (f *frontendCLI) NotifySilentUpdateError(err error) {}
|
||||
232
internal/frontend/cli-ie/importexport.go
Normal file
232
internal/frontend/cli-ie/importexport.go
Normal file
@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2021 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) {
|
||||
counts := progress.GetCounts()
|
||||
if counts.Total != 0 {
|
||||
f.Println(fmt.Sprintf(
|
||||
"Progress update: %d (%d / %d) / %d, skipped: %d, failed: %d",
|
||||
counts.Imported,
|
||||
counts.Exported,
|
||||
counts.Added,
|
||||
counts.Total,
|
||||
counts.Skipped,
|
||||
counts.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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
50
internal/frontend/cli-ie/system.go
Normal file
50
internal/frontend/cli-ie/system.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2021 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.restarter.SetToRestart()
|
||||
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) {
|
||||
if path, err := f.locations.ProvideLogsPath(); err != nil {
|
||||
f.Println("Failed to determine location of log files")
|
||||
} else {
|
||||
f.Println("Log files are stored in\n\n ", path)
|
||||
}
|
||||
}
|
||||
|
||||
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/")
|
||||
}
|
||||
35
internal/frontend/cli-ie/updates.go
Normal file
35
internal/frontend/cli-ie/updates.go
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2021 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/abiosoft/ishell"
|
||||
)
|
||||
|
||||
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
|
||||
f.Println("Your version is up to date.")
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printCredits(c *ishell.Context) {
|
||||
for _, pkg := range strings.Split(importexport.Credits, ";") {
|
||||
f.Println(pkg)
|
||||
}
|
||||
}
|
||||
128
internal/frontend/cli-ie/utils.go
Normal file
128
internal/frontend/cli-ie/utils.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2021 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() {
|
||||
version, err := f.updater.Check()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to notify need upgrade")
|
||||
return
|
||||
}
|
||||
f.Println("Please download and install the newest version of application from", version.LandingPage)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyCredentialsError() { // nolint[unused]
|
||||
// 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.
|
||||
`)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -55,7 +55,7 @@ func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ish
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
|
||||
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.User {
|
||||
user := f.getUserByIndexOrName("")
|
||||
if user != nil {
|
||||
return user
|
||||
@ -76,7 +76,7 @@ func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
|
||||
return user
|
||||
}
|
||||
|
||||
func (f *frontendCLI) getUserByIndexOrName(arg string) types.BridgeUser {
|
||||
func (f *frontendCLI) getUserByIndexOrName(arg string) types.User {
|
||||
users := f.bridge.GetUsers()
|
||||
numberOfAccounts := len(users)
|
||||
if numberOfAccounts == 0 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -21,8 +21,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
@ -63,23 +63,23 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) showAccountAddressInfo(user types.BridgeUser, address string) {
|
||||
func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) {
|
||||
smtpSecurity := "STARTTLS"
|
||||
if f.preferences.GetBool(preferences.SMTPSSLKey) {
|
||||
if f.settings.GetBool(settings.SMTPSSLKey) {
|
||||
smtpSecurity = "SSL"
|
||||
}
|
||||
f.Println(bold("Configuration for " + address))
|
||||
f.Printf("IMAP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
|
||||
bridge.Host,
|
||||
f.preferences.GetInt(preferences.IMAPPortKey),
|
||||
f.settings.GetInt(settings.IMAPPortKey),
|
||||
address,
|
||||
user.GetBridgePassword(),
|
||||
"STARTTLS",
|
||||
)
|
||||
f.Println("")
|
||||
f.Printf("SMTP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
|
||||
f.Printf("SMTP Settings\nAddress: %s\nSMTP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
|
||||
bridge.Host,
|
||||
f.preferences.GetInt(preferences.SMTPPortKey),
|
||||
f.settings.GetInt(settings.SMTPPortKey),
|
||||
address,
|
||||
user.GetBridgePassword(),
|
||||
smtpSecurity,
|
||||
@ -126,7 +126,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
return
|
||||
}
|
||||
|
||||
_, err = client.Auth2FA(twoFactor, auth)
|
||||
err = client.Auth2FA(twoFactor, auth)
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -19,10 +19,11 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
@ -36,34 +37,36 @@ var (
|
||||
type frontendCLI struct {
|
||||
*ishell.Shell
|
||||
|
||||
config *config.Config
|
||||
preferences *config.Preferences
|
||||
locations *locations.Locations
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
updates types.Updater
|
||||
updater types.Updater
|
||||
bridge types.Bridger
|
||||
|
||||
appRestart bool
|
||||
restarter types.Restarter
|
||||
}
|
||||
|
||||
// New returns a new CLI frontend configured with the given options.
|
||||
func New( //nolint[funlen]
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
preferences *config.Preferences,
|
||||
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
bridge types.Bridger,
|
||||
restarter types.Restarter,
|
||||
) *frontendCLI { //nolint[golint]
|
||||
fe := &frontendCLI{
|
||||
Shell: ishell.New(),
|
||||
|
||||
config: config,
|
||||
preferences: preferences,
|
||||
locations: locations,
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
updates: updates,
|
||||
updater: updater,
|
||||
bridge: bridge,
|
||||
|
||||
appRestart: false,
|
||||
restarter: restarter,
|
||||
}
|
||||
|
||||
// Clear commands.
|
||||
@ -77,7 +80,7 @@ func New( //nolint[funlen]
|
||||
Func: fe.deleteCache,
|
||||
})
|
||||
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
|
||||
Help: "remove all accounts from keychain. (aliases: k, keychain)",
|
||||
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
|
||||
Aliases: []string{"a", "k", "keychain"},
|
||||
Func: fe.deleteAccounts,
|
||||
})
|
||||
@ -135,11 +138,7 @@ func New( //nolint[funlen]
|
||||
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,
|
||||
@ -186,13 +185,12 @@ func New( //nolint[funlen]
|
||||
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)
|
||||
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
|
||||
internetOffCh := f.getEventChannel(events.InternetOffEvent)
|
||||
internetOnCh := f.getEventChannel(events.InternetOnEvent)
|
||||
addressChangedCh := f.getEventChannel(events.AddressChangedEvent)
|
||||
@ -203,6 +201,8 @@ func (f *frontendCLI) watchEvents() {
|
||||
select {
|
||||
case errorDetails := <-errorCh:
|
||||
f.Println("Bridge failed:", errorDetails)
|
||||
case <-credentialsErrorCh:
|
||||
f.notifyCredentialsError()
|
||||
case <-internetOffCh:
|
||||
f.notifyInternetOff()
|
||||
case <-internetOnCh:
|
||||
@ -226,23 +226,12 @@ func (f *frontendCLI) watchEvents() {
|
||||
func (f *frontendCLI) getEventChannel(event string) <-chan string {
|
||||
ch := make(chan string)
|
||||
f.eventListener.Add(event, ch)
|
||||
f.eventListener.RetryEmit(event)
|
||||
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.preferences.SetBool(preferences.FirstStartKey, false)
|
||||
|
||||
func (f *frontendCLI) Loop() error {
|
||||
f.Print(`
|
||||
Welcome to ProtonMail Bridge interactive shell
|
||||
___....___
|
||||
@ -263,3 +252,12 @@ func (f *frontendCLI) Loop(credentialsError error) error {
|
||||
f.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
|
||||
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
|
||||
}
|
||||
|
||||
func (f *frontendCLI) WaitUntilFrontendIsReady() {}
|
||||
func (f *frontendCLI) SetVersion(version updater.VersionInfo) {}
|
||||
func (f *frontendCLI) NotifySilentUpdateInstalled() {}
|
||||
func (f *frontendCLI) NotifySilentUpdateError(err error) {}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -22,7 +22,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
@ -34,7 +34,7 @@ var (
|
||||
func (f *frontendCLI) restart(c *ishell.Context) {
|
||||
if f.yesNoQuestion("Are you sure you want to restart the Bridge") {
|
||||
f.Println("Restarting Bridge...")
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
@ -43,12 +43,16 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
||||
if f.bridge.CheckConnection() == nil {
|
||||
f.Println("Internet connection is available.")
|
||||
} else {
|
||||
f.Println("Can not contact server please check you internet connection.")
|
||||
f.Println("Can not contact the server, please check your internet connection.")
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printLogDir(c *ishell.Context) {
|
||||
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
|
||||
if path, err := f.locations.ProvideLogsPath(); err != nil {
|
||||
f.Println("Failed to determine location of log files")
|
||||
} else {
|
||||
f.Println("Log files are stored in\n\n ", path)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printManual(c *ishell.Context) {
|
||||
@ -69,7 +73,7 @@ func (f *frontendCLI) deleteCache(c *ishell.Context) {
|
||||
f.Println("Cached cleared, restarting bridge")
|
||||
// Clearing data removes everything (db, preferences, ...)
|
||||
// so everything has to be stopped and started again.
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
|
||||
@ -77,7 +81,7 @@ func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
isSSL := f.preferences.GetBool(preferences.SMTPSSLKey)
|
||||
isSSL := f.settings.GetBool(settings.SMTPSSLKey)
|
||||
newSecurity := "SSL"
|
||||
if isSSL {
|
||||
newSecurity = "STARTTLS"
|
||||
@ -86,9 +90,9 @@ func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
|
||||
msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q and restart the Bridge", newSecurity)
|
||||
|
||||
if f.yesNoQuestion(msg) {
|
||||
f.preferences.SetBool(preferences.SMTPSSLKey, !isSSL)
|
||||
f.settings.SetBool(settings.SMTPSSLKey, !isSSL)
|
||||
f.Println("Restarting Bridge...")
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
@ -97,14 +101,14 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
currentPort = f.preferences.Get(preferences.IMAPPortKey)
|
||||
currentPort = f.settings.Get(settings.IMAPPortKey)
|
||||
newIMAPPort := f.readStringInAttempts("Set IMAP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
|
||||
if newIMAPPort == "" {
|
||||
newIMAPPort = currentPort
|
||||
}
|
||||
imapPortChanged := newIMAPPort != currentPort
|
||||
|
||||
currentPort = f.preferences.Get(preferences.SMTPPortKey)
|
||||
currentPort = f.settings.Get(settings.SMTPPortKey)
|
||||
newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
|
||||
if newSMTPPort == "" {
|
||||
newSMTPPort = currentPort
|
||||
@ -118,10 +122,10 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
|
||||
|
||||
if imapPortChanged || smtpPortChanged {
|
||||
f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort)
|
||||
f.preferences.Set(preferences.IMAPPortKey, newIMAPPort)
|
||||
f.preferences.Set(preferences.SMTPPortKey, newSMTPPort)
|
||||
f.settings.Set(settings.IMAPPortKey, newIMAPPort)
|
||||
f.settings.Set(settings.SMTPPortKey, newSMTPPort)
|
||||
f.Println("Restarting Bridge...")
|
||||
f.appRestart = true
|
||||
f.restarter.SetToRestart()
|
||||
f.Stop()
|
||||
} else {
|
||||
f.Println("Nothing changed")
|
||||
@ -129,16 +133,16 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
|
||||
if f.preferences.GetBool(preferences.AllowProxyKey) {
|
||||
if f.settings.GetBool(settings.AllowProxyKey) {
|
||||
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") {
|
||||
f.preferences.SetBool(preferences.AllowProxyKey, false)
|
||||
f.settings.SetBool(settings.AllowProxyKey, false)
|
||||
f.bridge.DisallowProxy()
|
||||
}
|
||||
} else {
|
||||
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") {
|
||||
f.preferences.SetBool(preferences.AllowProxyKey, true)
|
||||
f.settings.SetBool(settings.AllowProxyKey, true)
|
||||
f.bridge.AllowProxy()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -21,41 +21,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
|
||||
isUpToDate, latestVersionInfo, err := f.updates.CheckIsBridgeUpToDate()
|
||||
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 Bridge "+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)
|
||||
}
|
||||
f.Println("Your version is up to date.")
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printCredits(c *ishell.Context) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -93,10 +93,15 @@ func (f *frontendCLI) notifyLogout(address string) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyNeedUpgrade() {
|
||||
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
|
||||
version, err := f.updater.Check()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to notify need upgrade")
|
||||
return
|
||||
}
|
||||
f.Println("Please download and install the newest version of application from", version.LandingPage)
|
||||
}
|
||||
|
||||
func (f *frontendCLI) notifyCredentialsError() {
|
||||
func (f *frontendCLI) notifyCredentialsError() { // nolint[unused]
|
||||
// Print in 80-column width.
|
||||
f.Println("ProtonMail Bridge is not able to detect a supported password manager")
|
||||
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -19,12 +19,17 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"github.com/0xAX/notificator"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
|
||||
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
|
||||
qtie "github.com/ProtonMail/proton-bridge/internal/frontend/qt-ie"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -35,54 +40,161 @@ var (
|
||||
|
||||
// Frontend is an interface to be implemented by each frontend type (cli, gui, html).
|
||||
type Frontend interface {
|
||||
Loop(credentialsError error) error
|
||||
IsAppRestarting() bool
|
||||
}
|
||||
|
||||
// HandlePanic handles panics which occur for users with GUI.
|
||||
func HandlePanic() {
|
||||
notify := notificator.New(notificator.Options{
|
||||
DefaultIcon: "../frontend/ui/icon/icon.png",
|
||||
AppName: "ProtonMail Bridge",
|
||||
})
|
||||
_ = notify.Push("Fatal Error", "The ProtonMail Bridge has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
|
||||
Loop() error
|
||||
NotifyManualUpdate(update updater.VersionInfo, canInstall bool)
|
||||
SetVersion(update updater.VersionInfo)
|
||||
NotifySilentUpdateInstalled()
|
||||
NotifySilentUpdateError(error)
|
||||
WaitUntilFrontendIsReady()
|
||||
}
|
||||
|
||||
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
|
||||
func New(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType string,
|
||||
showWindowOnStart bool,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
preferences *config.Preferences,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
bridge *bridge.Bridge,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
restarter types.Restarter,
|
||||
) Frontend {
|
||||
bridgeWrap := types.NewBridgeWrap(bridge)
|
||||
return new(version, buildVersion, frontendType, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridgeWrap, noEncConfirmator)
|
||||
return newBridgeFrontend(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType,
|
||||
showWindowOnStart,
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
bridgeWrap,
|
||||
noEncConfirmator,
|
||||
autostart,
|
||||
restarter,
|
||||
)
|
||||
}
|
||||
|
||||
func new(
|
||||
func newBridgeFrontend(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType string,
|
||||
showWindowOnStart bool,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
preferences *config.Preferences,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
updater types.Updater,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
restarter types.Restarter,
|
||||
) Frontend {
|
||||
switch frontendType {
|
||||
case "cli":
|
||||
return cli.New(panicHandler, config, preferences, eventListener, updates, bridge)
|
||||
return cli.New(
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
bridge,
|
||||
restarter,
|
||||
)
|
||||
default:
|
||||
return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
|
||||
return qt.New(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
showWindowOnStart,
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
bridge,
|
||||
noEncConfirmator,
|
||||
autostart,
|
||||
restarter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// NewImportExport returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
|
||||
func NewImportExport(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType string,
|
||||
panicHandler types.PanicHandler,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
ie *importexport.ImportExport,
|
||||
restarter types.Restarter,
|
||||
) Frontend {
|
||||
ieWrap := types.NewImportExportWrap(ie)
|
||||
return newIEFrontend(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType,
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
ieWrap,
|
||||
restarter,
|
||||
)
|
||||
}
|
||||
|
||||
func newIEFrontend(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
frontendType string,
|
||||
panicHandler types.PanicHandler,
|
||||
locations *locations.Locations,
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
ie types.ImportExporter,
|
||||
restarter types.Restarter,
|
||||
) Frontend {
|
||||
switch frontendType {
|
||||
case "cli":
|
||||
return cliie.New(
|
||||
panicHandler,
|
||||
locations,
|
||||
eventListener,
|
||||
updater,
|
||||
ie,
|
||||
restarter,
|
||||
)
|
||||
default:
|
||||
return qtie.New(
|
||||
version,
|
||||
buildVersion,
|
||||
programName,
|
||||
panicHandler,
|
||||
locations,
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
ie,
|
||||
restarter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -226,7 +226,7 @@ Dialog {
|
||||
target: timer
|
||||
onTriggered: {
|
||||
go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked)
|
||||
go.isRestarting = true
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -137,6 +137,7 @@ Dialog {
|
||||
spacing: Style.dialog.spacing
|
||||
ButtonRounded {
|
||||
id:buttonNo
|
||||
visible: root.state != "toggleEarlyAccess"
|
||||
color_main: Style.dialog.text
|
||||
fa_icon: Style.fa.times
|
||||
text: qsTr("No")
|
||||
@ -148,7 +149,7 @@ Dialog {
|
||||
color_minor: Style.main.textBlue
|
||||
isOpaque: true
|
||||
fa_icon: Style.fa.check
|
||||
text: qsTr("Yes")
|
||||
text: root.state == "toggleEarlyAccess" ? qsTr("Ok") : qsTr("Yes")
|
||||
onClicked : {
|
||||
currentIndex=1
|
||||
root.confirmed()
|
||||
@ -292,6 +293,28 @@ Dialog {
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "toggleEarlyAccessOn"
|
||||
PropertyChanges {
|
||||
target: root
|
||||
currentIndex : 0
|
||||
question : qsTr("Do you want to be the first to get the latest updates? Please keep in mind that early versions may be less stable.")
|
||||
note : ""
|
||||
title : qsTr("Enable early access")
|
||||
answer : qsTr("Enabling early access...")
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "toggleEarlyAccessOff"
|
||||
PropertyChanges {
|
||||
target: root
|
||||
currentIndex : 0
|
||||
question : qsTr("Are you sure you want to leave early access? Please keep in mind this operation clears the cache and restarts Bridge.")
|
||||
note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, temporarily slowing down the email download process significantly.")
|
||||
title : qsTr("Disable early access")
|
||||
answer : qsTr("Disabling early access...")
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "noKeychain"
|
||||
PropertyChanges {
|
||||
@ -340,13 +363,9 @@ Dialog {
|
||||
winMain.dialogAddUser .visible = false
|
||||
winMain.dialogChangePort .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
|
||||
@ -360,17 +379,19 @@ Dialog {
|
||||
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 == "toggleAllowProxy" ) { go.toggleAllowProxy () }
|
||||
if ( state == "quit" ) { Qt.quit () }
|
||||
if ( state == "instance exists" ) { Qt.quit () }
|
||||
if ( state == "noKeychain" ) { Qt.quit () }
|
||||
if ( state == "checkUpdates" ) { go.runCheckVersion (true) }
|
||||
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 == "toggleAllowProxy" ) { go.toggleAllowProxy () }
|
||||
if ( state == "toggleEarlyAccessOn" ) { go.toggleEarlyAccess () }
|
||||
if ( state == "toggleEarlyAccessOff" ) { go.toggleEarlyAccess () }
|
||||
if ( state == "quit" ) { Qt.quit () }
|
||||
if ( state == "instance exists" ) { Qt.quit () }
|
||||
if ( state == "noKeychain" ) { Qt.quit () }
|
||||
if ( state == "checkUpdates" ) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -74,9 +74,7 @@ Item {
|
||||
rightIcon.text : Style.fa.chevron_circle_right
|
||||
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
|
||||
onClicked: {
|
||||
dialogGlobal.state="checkUpdates"
|
||||
dialogGlobal.show()
|
||||
dialogGlobal.confirmed()
|
||||
go.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +100,7 @@ Item {
|
||||
Row {
|
||||
anchors.left : parent.left
|
||||
|
||||
Rectangle { height: Style.dialog.spacing; width: (wrapper.width- credits.width - release.width - sepaCreditsRelease.width)/2; color: "transparent"}
|
||||
Rectangle { height: Style.dialog.spacing; width: (wrapper.width - credits.width - licenseFile.width - release.width - sepaCreditsRelease.width)/2; color: "transparent"}
|
||||
|
||||
ClickIconText {
|
||||
id:credits
|
||||
@ -114,6 +112,20 @@ Item {
|
||||
onClicked : winMain.dialogCredits.show()
|
||||
}
|
||||
|
||||
Rectangle {id: sepaLicenseFile ; height: Style.dialog.spacing; width: Style.main.dummy; color: "transparent"}
|
||||
|
||||
ClickIconText {
|
||||
id:licenseFile
|
||||
iconText : ""
|
||||
text : qsTr("License", "link to click on to view license file")
|
||||
textColor : Style.main.textDisabled
|
||||
fontSize : Style.main.fontSize
|
||||
textUnderline : true
|
||||
onClicked : {
|
||||
go.openLicenseFile()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {id: sepaCreditsRelease ; height: Style.dialog.spacing; width: Style.main.dummy; color: "transparent"}
|
||||
|
||||
ClickIconText {
|
||||
@ -123,10 +135,7 @@ Item {
|
||||
textColor : Style.main.textDisabled
|
||||
fontSize : Style.main.fontSize
|
||||
textUnderline : true
|
||||
onClicked : {
|
||||
go.getLocalVersionInfo()
|
||||
winMain.dialogVersionInfo.show()
|
||||
}
|
||||
onClicked : gui.openReleaseNotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -38,7 +38,6 @@ Window {
|
||||
property alias dialogUpdate : dialogUpdate
|
||||
property alias dialogFirstStart : dialogFirstStart
|
||||
property alias dialogGlobal : dialogGlobal
|
||||
property alias dialogVersionInfo : dialogVersionInfo
|
||||
property alias dialogConnectionTroubleshoot : dialogConnectionTroubleshoot
|
||||
property alias bubbleNote : bubbleNote
|
||||
property alias addAccountTip : addAccountTip
|
||||
@ -66,7 +65,6 @@ Window {
|
||||
!dialogUpdate .visible &&
|
||||
!dialogFirstStart .visible &&
|
||||
!dialogGlobal .visible &&
|
||||
!dialogVersionInfo .visible &&
|
||||
!bubbleNote .visible
|
||||
|
||||
Accessible.role: Accessible.Grouping
|
||||
@ -316,50 +314,7 @@ Window {
|
||||
|
||||
DialogUpdate {
|
||||
id: dialogUpdate
|
||||
|
||||
property string manualLinks : {
|
||||
var out = ""
|
||||
var links = go.downloadLink.split("\n")
|
||||
var l;
|
||||
for (l in links) {
|
||||
out += '<a href="%1">%1</a><br>'.arg(links[l])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
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 download and install the latest version to continue using %1.<br><br>
|
||||
%2',
|
||||
"Message for force-update in Linux").arg(go.programTitle).arg(dialogUpdate.manualLinks)
|
||||
} else {
|
||||
return qsTr('You are using an outdated version of our software.<br>
|
||||
Please download and install the latest version to continue using %1.<br><br>
|
||||
You can continue with the 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 Bridge is available.<br>
|
||||
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
|
||||
Use your package manager to update or download and install the new version manually from<br><br>
|
||||
%3',
|
||||
"Message for update in Linux").arg("releaseNotes").arg(go.newversion).arg(dialogUpdate.manualLinks)
|
||||
} else {
|
||||
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>
|
||||
You can continue with the update or download and install new version manually from<br><br>
|
||||
<a href="%3">%3</a>',
|
||||
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
forceUpdate: root.isOutdateVersion
|
||||
}
|
||||
|
||||
|
||||
@ -373,25 +328,6 @@ Window {
|
||||
id: dialogTlsCert
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: dialogVersionInfo
|
||||
property bool checkVersionOnClose : false
|
||||
title: qsTr("Information about", "title of release notes page") + " v" + go.newversion
|
||||
VersionInfo { }
|
||||
onShow : {
|
||||
// Hide information bar with old version
|
||||
if (infoBar.state=="oldVersion") {
|
||||
infoBar.state="upToDate"
|
||||
dialogVersionInfo.checkVersionOnClose = true
|
||||
}
|
||||
}
|
||||
onHide : {
|
||||
// Reload current version based on online status
|
||||
if (dialogVersionInfo.checkVersionOnClose) go.runCheckVersion(false)
|
||||
dialogVersionInfo.checkVersionOnClose = false
|
||||
}
|
||||
}
|
||||
|
||||
DialogYesNo {
|
||||
id: dialogGlobal
|
||||
question : ""
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -84,7 +84,7 @@ Window {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
ButtonRounded {
|
||||
id: cancel
|
||||
id: sendAnyway
|
||||
onClicked : root.hide(true)
|
||||
height: Style.main.fontSize*2
|
||||
//width: Style.dialog.widthButton*1.3
|
||||
@ -93,7 +93,7 @@ Window {
|
||||
}
|
||||
|
||||
ButtonRounded {
|
||||
id: sendAnyway
|
||||
id: cancel
|
||||
onClicked : root.hide(false)
|
||||
height: Style.main.fontSize*2
|
||||
//width: Style.dialog.widthButton*1.3
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -36,6 +36,24 @@ Item {
|
||||
color: Style.main.background
|
||||
}
|
||||
|
||||
// horizontall scrollbar sometimes showes up when vertical scrollbar coveres content
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
|
||||
// keeping vertical scrollbar allways visible when needed
|
||||
Connections {
|
||||
target: wrapper.ScrollBar.vertical
|
||||
onSizeChanged: {
|
||||
// ScrollBar.size == 0 at creating so no need to make it active
|
||||
if (wrapper.ScrollBar.vertical.size < 1.0 && wrapper.ScrollBar.vertical.size > 0 && !wrapper.ScrollBar.vertical.active) {
|
||||
wrapper.ScrollBar.vertical.active = true
|
||||
}
|
||||
}
|
||||
onActiveChanged: {
|
||||
wrapper.ScrollBar.vertical.active = true
|
||||
}
|
||||
}
|
||||
|
||||
// content
|
||||
Column {
|
||||
anchors.left : parent.left
|
||||
@ -48,6 +66,7 @@ Item {
|
||||
text : qsTr("Clear", "clickable link next to clear cache button in settings")
|
||||
color: Style.main.text
|
||||
font {
|
||||
family : cacheClear.font.family // use default font, not font-awesome
|
||||
pointSize : Style.settings.fontSize * Style.pt
|
||||
underline : true
|
||||
}
|
||||
@ -66,6 +85,7 @@ Item {
|
||||
text : qsTr("Clear", "clickable link next to clear keychain button in settings")
|
||||
color: Style.main.text
|
||||
font {
|
||||
family : cacheKeychain.font.family // use default font, not font-awesome
|
||||
pointSize : Style.settings.fontSize * Style.pt
|
||||
underline : true
|
||||
}
|
||||
@ -95,6 +115,50 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ButtonIconText {
|
||||
id: autoUpdates
|
||||
text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates")
|
||||
leftIcon.text : Style.fa.download
|
||||
rightIcon {
|
||||
font.pointSize : Style.settings.toggleSize * Style.pt
|
||||
text : go.isAutoUpdate!=false ? Style.fa.toggle_on : Style.fa.toggle_off
|
||||
color : go.isAutoUpdate!=false ? Style.main.textBlue : Style.main.textDisabled
|
||||
}
|
||||
Accessible.description: (
|
||||
go.isAutoUpdate == false ?
|
||||
qsTr("Enable" , "Click to enable the automatic update of Bridge") :
|
||||
qsTr("Disable" , "Click to disable the automatic update of Bridge")
|
||||
) + " " + text
|
||||
onClicked: {
|
||||
go.toggleAutoUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
ButtonIconText {
|
||||
id: earlyAccess
|
||||
text: qsTr("Early access", "label for toggle that enables and disables early access")
|
||||
leftIcon.text : Style.fa.star
|
||||
rightIcon {
|
||||
font.pointSize : Style.settings.toggleSize * Style.pt
|
||||
text : go.isEarlyAccess!=false ? Style.fa.toggle_on : Style.fa.toggle_off
|
||||
color : go.isEarlyAccess!=false ? Style.main.textBlue : Style.main.textDisabled
|
||||
}
|
||||
Accessible.description: (
|
||||
go.isEarlyAccess == false ?
|
||||
qsTr("Enable" , "Click to enable early access") :
|
||||
qsTr("Disable" , "Click to disable early access")
|
||||
) + " " + text
|
||||
onClicked: {
|
||||
if (go.isEarlyAccess == true) {
|
||||
dialogGlobal.state="toggleEarlyAccessOff"
|
||||
dialogGlobal.show()
|
||||
} else {
|
||||
dialogGlobal.state="toggleEarlyAccessOn"
|
||||
dialogGlobal.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonIconText {
|
||||
id: advancedSettings
|
||||
property bool isAdvanced : !go.isDefaultPort
|
||||
@ -125,6 +189,7 @@ Item {
|
||||
text : qsTr("Change", "clickable link next to change ports button in settings")
|
||||
color: Style.main.text
|
||||
font {
|
||||
family : changePort.font.family // use default font, not font-awesome
|
||||
pointSize : Style.settings.fontSize * Style.pt
|
||||
underline : true
|
||||
}
|
||||
@ -174,7 +239,6 @@ Item {
|
||||
dialogGlobal.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
|
||||
@ -1,127 +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/>.
|
||||
|
||||
// credits
|
||||
|
||||
import QtQuick 2.8
|
||||
import BridgeUI 1.0
|
||||
import ProtonUI 1.0
|
||||
|
||||
Item {
|
||||
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: Style.dialog.spacing
|
||||
|
||||
AccessibleText {
|
||||
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", "list of release notes for this version of the app") + ":"
|
||||
}
|
||||
|
||||
AccessibleSelectableText {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Style.main.leftMargin
|
||||
}
|
||||
font {
|
||||
pointSize : Style.main.fontSize * Style.pt
|
||||
}
|
||||
width: wrapper.width - anchors.leftMargin
|
||||
onLinkActivated: {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
wrapMode: Text.Wrap
|
||||
color: Style.main.text
|
||||
text: go.changelog
|
||||
}
|
||||
|
||||
AccessibleText {
|
||||
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", "list of bugs fixed for this version of the app") + ":"
|
||||
}
|
||||
|
||||
AccessibleSelectableText {
|
||||
visible: go.bugfixes!=""
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Style.main.leftMargin
|
||||
}
|
||||
font {
|
||||
pointSize : Style.main.fontSize * Style.pt
|
||||
}
|
||||
width: wrapper.width - anchors.leftMargin
|
||||
onLinkActivated: {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
wrapMode: Text.Wrap
|
||||
color: Style.main.text
|
||||
text: go.bugfixes
|
||||
}
|
||||
|
||||
Rectangle{id:spacer; color:Style.transparent; width: Style.main.dummy; height: buttonClose.height}
|
||||
|
||||
ButtonRounded {
|
||||
id: buttonClose
|
||||
anchors.horizontalCenter: content.horizontalCenter
|
||||
text: qsTr("Close")
|
||||
onClicked: {
|
||||
dialogVersionInfo.hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AccessibleSelectableText {
|
||||
anchors.horizontalCenter: content.horizontalCenter
|
||||
font {
|
||||
pointSize : Style.main.fontSize * Style.pt
|
||||
}
|
||||
color: Style.main.textDisabled
|
||||
text: "\n Current: "+go.fullversion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,4 +12,3 @@ ManualWindow 1.0 ManualWindow.qml
|
||||
OutgoingNoEncPopup 1.0 OutgoingNoEncPopup.qml
|
||||
SettingsView 1.0 SettingsView.qml
|
||||
StatusFooter 1.0 StatusFooter.qml
|
||||
VersionInfo 1.0 VersionInfo.qml
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
@ -54,13 +54,15 @@ Item {
|
||||
onWarningFlagsChanged : {
|
||||
if (gui.warningFlags==Style.okInfoBar) {
|
||||
go.normalSystray()
|
||||
} else {
|
||||
if ((gui.warningFlags & Style.errorInfoBar) == Style.errorInfoBar) {
|
||||
go.errorSystray()
|
||||
} else {
|
||||
go.highlightSystray()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ((gui.warningFlags & Style.errorInfoBar) == Style.errorInfoBar) {
|
||||
go.errorSystray()
|
||||
return
|
||||
}
|
||||
|
||||
go.highlightSystray()
|
||||
}
|
||||
|
||||
// Signals from Go
|
||||
@ -112,14 +114,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onRunCheckVersion : {
|
||||
gui.openMainWindow(false)
|
||||
go.setUpdateState("upToDate")
|
||||
winMain.dialogGlobal.state="checkUpdates"
|
||||
winMain.dialogGlobal.show()
|
||||
go.isNewVersionAvailable(showMessage)
|
||||
}
|
||||
|
||||
onSetUpdateState : {
|
||||
// once app is outdated prevent from state change
|
||||
if (winMain.updateState != "forceUpdate") {
|
||||
@ -134,15 +128,49 @@ Item {
|
||||
go.silentBubble(2,qsTr("You have the latest version!", "notification", -1))
|
||||
}
|
||||
|
||||
onNotifyUpdate : {
|
||||
onNotifyManualUpdate: {
|
||||
go.setUpdateState("oldVersion")
|
||||
}
|
||||
|
||||
onNotifyManualUpdateRestartNeeded: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateRestart")
|
||||
winMain.dialogUpdate.finished(false)
|
||||
|
||||
// after manual update - just retart immidiatly
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
}
|
||||
|
||||
onNotifyManualUpdateError: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateError")
|
||||
winMain.dialogUpdate.finished(true)
|
||||
}
|
||||
|
||||
onNotifyForceUpdate : {
|
||||
go.setUpdateState("forceUpdate")
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
go.runCheckVersion(false)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
}
|
||||
|
||||
onNotifySilentUpdateRestartNeeded: {
|
||||
go.setUpdateState("updateRestart")
|
||||
}
|
||||
|
||||
onNotifySilentUpdateError: {
|
||||
go.setUpdateState("updateError")
|
||||
gui.openMainWindow(true)
|
||||
}
|
||||
|
||||
onNotifyLogout : {
|
||||
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Bridge with this account.").arg(accname) )
|
||||
}
|
||||
@ -229,30 +257,14 @@ Item {
|
||||
outgoingNoEncPopup.y = y
|
||||
}
|
||||
|
||||
onUpdateFinished : {
|
||||
winMain.dialogUpdate.finished(hasError)
|
||||
}
|
||||
|
||||
onShowCertIssue : {
|
||||
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"
|
||||
))
|
||||
onOpenReleaseNotesExternally: {
|
||||
Qt.openUrlExternally(go.updateReleaseNotesLink)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: checkVersionTimer
|
||||
repeat : true
|
||||
triggeredOnStart: false
|
||||
interval : Style.main.verCheckRepeatTime
|
||||
onTriggered : go.runCheckVersion(false)
|
||||
}
|
||||
|
||||
function openMainWindow(showAndRise) {
|
||||
@ -294,6 +306,15 @@ Item {
|
||||
winMain.bubbleNote.show()
|
||||
}
|
||||
|
||||
function openReleaseNotes(){
|
||||
if (go.updateReleaseNotesLink == "") {
|
||||
go.checkAndOpenReleaseNotes()
|
||||
return
|
||||
}
|
||||
go.openReleaseNotesExternally()
|
||||
}
|
||||
|
||||
|
||||
// On start
|
||||
Component.onCompleted : {
|
||||
// set messages for translations
|
||||
@ -306,17 +327,12 @@ Item {
|
||||
go.failedAutostart = qsTr("Unable to configure automatic start." , "notification", -1)
|
||||
go.genericErrSeeLogs = qsTr("An error happened during procedure. See logs for more details." , "notification", -1)
|
||||
|
||||
go.guiIsReady()
|
||||
|
||||
// start window
|
||||
gui.openMainWindow(false)
|
||||
checkVersionTimer.start()
|
||||
if (go.isShownOnStart) {
|
||||
gui.winMain.showAndRise()
|
||||
}
|
||||
go.runCheckVersion(false)
|
||||
|
||||
if (go.isFreshVersion) {
|
||||
go.getLocalVersionInfo()
|
||||
gui.winMain.dialogVersionInfo.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
441
internal/frontend/qml/GuiIE.qml
Normal file
441
internal/frontend/qml/GuiIE.qml
Normal file
@ -0,0 +1,441 @@
|
||||
// Copyright (c) 2021 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
|
||||
|
||||
onShowWindow : {
|
||||
winMain.showAndRise()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
onSetUpdateState : {
|
||||
// once app is outdated prevent from state change
|
||||
if (winMain.updateState != "forceUpdate") {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
onNotifyManualUpdate: {
|
||||
go.setUpdateState("oldVersion")
|
||||
}
|
||||
|
||||
onNotifyManualUpdateRestartNeeded: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateRestart")
|
||||
winMain.dialogUpdate.finished(false)
|
||||
|
||||
// after manual update - just retart immidiatly
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
}
|
||||
|
||||
onNotifyManualUpdateError: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateError")
|
||||
winMain.dialogUpdate.finished(true)
|
||||
}
|
||||
|
||||
onNotifyForceUpdate : {
|
||||
go.setUpdateState("forceUpdate")
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
}
|
||||
|
||||
onNotifySilentUpdateRestartNeeded: {
|
||||
go.setUpdateState("updateRestart")
|
||||
}
|
||||
|
||||
onNotifySilentUpdateError: {
|
||||
go.setUpdateState("updateError")
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
onUpdateFinished : {
|
||||
winMain.dialogUpdate.finished(hasError)
|
||||
}
|
||||
|
||||
onOpenReleaseNotesExternally: {
|
||||
Qt.openUrlExternally(go.updateReleaseNotesLink)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function openReleaseNotes(){
|
||||
if (go.updateReleaseNotesLink == "") {
|
||||
go.checkAndOpenReleaseNotes()
|
||||
return
|
||||
}
|
||||
go.openReleaseNotesExternally()
|
||||
}
|
||||
|
||||
|
||||
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.guiIsReady()
|
||||
|
||||
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) 2021 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) 2021 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) 2021 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) 2021 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) 2021 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) 2021 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) 2021 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()
|
||||
}
|
||||
}
|
||||
459
internal/frontend/qml/ImportExportUI/DialogExport.qml
Normal file
459
internal/frontend/qml/ImportExportUI/DialogExport.qml
Normal file
@ -0,0 +1,459 @@
|
||||
// Copyright (c) 2021 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) {
|
||||
if (go.progressDescription=="") return qsTr("Export finished","todo")
|
||||
else return qsTr("Export failed: %1").arg(go.progressDescription)
|
||||
}
|
||||
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 : {
|
||||
errorPopup.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
1044
internal/frontend/qml/ImportExportUI/DialogImport.qml
Normal file
1044
internal/frontend/qml/ImportExportUI/DialogImport.qml
Normal file
File diff suppressed because it is too large
Load Diff
353
internal/frontend/qml/ImportExportUI/DialogYesNo.qml
Normal file
353
internal/frontend/qml/ImportExportUI/DialogYesNo.qml
Normal file
@ -0,0 +1,353 @@
|
||||
// Copyright (c) 2021 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
|
||||
// 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" ) { }
|
||||
}
|
||||
}
|
||||
|
||||
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) 2021 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) 2021 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user