Compare commits

..

1 Commits

Author SHA1 Message Date
61c6867a15 Update issue templates
General issue template
2020-04-29 07:50:30 +02:00
789 changed files with 16399 additions and 57635 deletions

2
.gitattributes vendored
View File

@ -1 +1 @@
unreleased.md merge=union Changelog.md merge=union

View File

@ -27,9 +27,6 @@ Issue tracker is ONLY used for reporting bugs with technical details. "It doesn'
3. 3.
4. 4.
## Version Information
<!--- Which version of the app(s) were you using when you experienced this issue? -->
## Context (Environment) ## Context (Environment)
<!--- How has this issue affected you? What are you trying to accomplish? --> <!--- 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 --> <!--- Providing context helps us come up with a solution that is most useful in the real world -->

37
.gitignore vendored
View File

@ -18,37 +18,8 @@ coverage.html
mem.pprof mem.pprof
# Auto generated frontend # Auto generated frontend
internal/frontend/qml/BridgeUI/*.qmlc frontend/qml/BridgeUI/*.qmlc
internal/frontend/qml/ImportExportUI/*.qmlc frontend/qml/ProtonUI/*.qmlc
internal/frontend/qml/ProtonUI/*.qmlc frontend/qml/ProtonUI/fontawesome.ttf
internal/frontend/qml/ProtonUI/fontawesome.ttf frontend/qml/ProtonUI/images
internal/frontend/qml/ProtonUI/images
internal/frontend/qml/ImportExportUI/images
frontend/qml/*.qmlc frontend/qml/*.qmlc
# Credits files (generated).
internal/**/credits.go
# Build files
/launcher-*
/bridge_*_*.tgz
/ie_*_*.tgz
/versioner
/hasher
cmd/Desktop-Bridge/deploy
cmd/Import-Export/deploy
internal/frontend/qt*/moc.cpp
internal/frontend/qt*/moc.go
internal/frontend/qt*/moc.h
internal/frontend/qt*/moc_cgo_*.go
internal/frontend/qt*/moc_moc.h
internal/frontend/qt*/rcc.cpp
internal/frontend/qt*/rcc.qrc
internal/frontend/qt*/rcc_cgo_*.go
internal/frontend/rcc.cpp
internal/frontend/rcc.qrc
internal/frontend/rcc_cgo_*.go
vendor-cache/
/main.go

View File

@ -1,4 +1,4 @@
image: gitlab.protontech.ch:4567/go/bridge-internal:latest image: gitlab.protontech.ch:4567/go/bridge/ci
before_script: before_script:
- eval $(ssh-agent -s) - eval $(ssh-agent -s)
@ -17,11 +17,32 @@ cache:
policy: pull policy: pull
stages: stages:
- image
- cache - cache
- test - test
- build - build
- mirror - mirror
# Stage: IMAGE
build-ci-image:
stage: image
image: docker:stable
before_script: []
cache: {}
tags:
- heavy
only:
changes:
- ci/*
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker info
- docker build -t gitlab.protontech.ch:4567/go/bridge/ci:latest ci
- docker push gitlab.protontech.ch:4567/go/bridge/ci:latest
# Stage: CACHE # Stage: CACHE
# This will ensure latest dependency versions and updates the cache for # This will ensure latest dependency versions and updates the cache for
@ -44,9 +65,7 @@ lint:
only: only:
- branches - branches
script: script:
- env GOMAXPROCS=$(( ${CI_TAG_CPU} / 2 )) make lint - make lint
tags:
- medium
test: test:
stage: test stage: test
@ -62,8 +81,6 @@ test:
- pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'` - pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'`
# Then finally run the tests # Then finally run the tests
- make test - make test
tags:
- medium
test-integration: test-integration:
stage: test stage: test
@ -71,8 +88,6 @@ test-integration:
- branches - branches
script: script:
- VERBOSITY=debug make -C test test - VERBOSITY=debug make -C test test
tags:
- large
dependency-updates: dependency-updates:
stage: test stage: test
@ -81,184 +96,18 @@ dependency-updates:
# Stage: BUILD # Stage: BUILD
.build-base: build-linux:
stage: build stage: build
# Test build every time (= we want to know build is possible).
only: only:
- branches - branches
script: script:
- make build - make build
artifacts: artifacts:
# Note: The latest artifacts for refs are locked against deletion, and kept regardless of the expiry time. name: "bridge-linux-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA"
# 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: paths:
- bridge_*.tgz - bridge_*.tgz
expire_in: 2 week
build-linux-qa:
extends: .build-base
only:
- 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
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOPATH=~/go
- export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
cache: {}
tags:
- macOS
build-darwin:
extends: .build-darwin-base
artifacts:
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-darwin-qa:
extends: .build-darwin-base
only:
- web
script:
- BUILD_TAGS="build_qa" 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.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows make build
artifacts:
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-windows-qa:
extends: .build-windows-base
only:
- web
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows BUILD_TAGS="build_qa" 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: mirror-repo:
stage: mirror stage: mirror

View File

@ -1,4 +1,3 @@
---
run: run:
timeout: 10m timeout: 10m
build-tags: build-tags:
@ -10,10 +9,8 @@ issues:
exclude-use-default: false exclude-use-default: false
exclude: exclude:
- Using the variable on range scope `tt` in function literal - Using the variable on range scope `tt` in function literal
# For now we are missing a lot of comments. - should have comment (\([^)]+\) )?or be unexported # For now we are missing a lot of comments.
- should have comment (\([^)]+\) )?or be unexported - at least one file in a package should have a package comment # For now we are missing a lot of comments.
# For now we are missing a lot of comments.
- at least one file in a package should have a package comment
exclude-rules: exclude-rules:
- path: _test\.go - path: _test\.go
@ -24,12 +21,6 @@ issues:
- gochecknoinits - gochecknoinits
- gosec - gosec
linters-settings:
godox:
keywords:
- TODO
- FIXME
linters: linters:
# setting disable-all will make only explicitly enabled linters run # setting disable-all will make only explicitly enabled linters run
disable-all: true disable-all: true
@ -52,6 +43,7 @@ linters:
- funlen # Tool for detection of long functions [fast: true, auto-fix: false] - funlen # Tool for detection of long functions [fast: true, auto-fix: false]
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false] - gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
#- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
- gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false] - gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false]
- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
@ -60,52 +52,15 @@ linters:
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] - golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false]
- gosec # Inspects source code for security problems [fast: true, auto-fix: false] - gosec # Inspects source code for security problems [fast: true, auto-fix: false]
- interfacer # Linter that suggests narrower interface types [fast: true, auto-fix: false]
- maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: true, auto-fix: false]
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
- prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false]
- scopelint # Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false]
- stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false] - stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false]
- unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false] - unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false]
- unparam # Reports unused function parameters [fast: true, auto-fix: false] - unparam # Reports unused function parameters [fast: true, auto-fix: false]
- whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true]
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
- exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false]
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
- godot # Check if comments end in a period [fast: true, auto-fix: true]
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: false]
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false]
- goprintffuncname # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false]
- importas # Enforces consistent import aliases [fast: false, auto-fix: false]
- makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false]
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
- predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false]
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
- rowserrcheck # checks whether Err of rows is checked successfully [fast: false, auto-fix: false]
- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false]
- tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false]
- wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false]
#- wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] #- wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false]
#- lll # Reports long lines [fast: true, auto-fix: false] #- lll # Reports long lines [fast: true, auto-fix: false]
# Consider to include:
# - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
# - cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false]
# - errorlint # go-errorlint is a source code linter for Go software that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false]
# - exhaustivestruct # Checks if all struct's fields are initialized [fast: false, auto-fix: false]
# - forbidigo # Forbids identifiers [fast: true, auto-fix: false]
# - gci # Gci control golang package import order and make it always deterministic. [fast: true, auto-fix: true]
# - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
# - goerr113 # Golang linter to check the errors handling expressions [fast: false, auto-fix: false]
# - gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
# - gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
# - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false]
# - ifshort # Checks that your code uses short syntax for if-statements whenever possible [fast: true, auto-fix: false]
# - nestif # Reports deeply nested if statements [fast: true, auto-fix: false]
# - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false]
# - noctx # noctx finds sending http request without context.Context [fast: false, auto-fix: false]
# - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false]
# - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test [fast: true, auto-fix: false]
# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
# - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]

View File

@ -1,35 +1,23 @@
# Building ProtonMail Bridge and Import-Export app # Building ProtonMail Bridge app
## Prerequisites ## Prerequisites
* 64-bit OS (the go-rfc5322 module cannot currently be compiled for 32-bit OSes)
* Go 1.13 * Go 1.13
* Bash with basic build utils: make, gcc, sed, find, grep, ... * 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/) * For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (linux, windows) or Xcode (macOS) * GCC (linux, windows) or Xcode (macOS)
* Windres (windows) * Windres (windows)
* libglvnd and libsecret development files (linux)
To enable the sending of crash reports using Sentry please set the To enable the sending of crash reports using Sentry please set the
`main.DSNSentry` value with the client key of your sentry project before build. `main.DSNSentry` value with the client key of your sentry project before build.
Otherwise, the sending of crash reports will be disabled. Otherwise, the sending of crash reports will be disabled.
## Build ## Build
In order to build Bridge or Import-Export app with Qt interface we are using * for Windows please unset the `MSYSTEM` variable
[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 ```bash
export MSYSTEM= export MSYSTEM=
``` ```
### Build Bridge
* in project root run * in project root run
```bash ```bash
@ -37,44 +25,9 @@ make build
``` ```
* The result will be stored in `./cmd/Destop-Bridge/deploy/${GOOS}/` * The result will be stored in `./cmd/Destop-Bridge/deploy/${GOOS}/`
* for `linux`, the binary will have the name of the project directory (e.g `proton-bridge`) * 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 `proton-bridge.exe`) * 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 `proton-bridge.app`) * for `darwin`, the application will be created with name of the project directory (e.g `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 ## Useful tests, lints and checks
In order to be able to run following commands please install the development dependencies: In order to be able to run following commands please install the development dependencies:
@ -82,5 +35,5 @@ In order to be able to run following commands please install the development dep
* `make test` will run all unit tests * `make test` will run all unit tests
* `make lint` will lint the whole project * `make lint` will lint the whole project
* `make -C ./test test` will run the integration tests * `make -C ./tests test` will run the integration tests
* `make run` will build Bridge without a GUI and start it in CLI mode * `make run` will build Bridge without a GUI and start it in CLI mode

73
COPYING.md Normal file
View File

@ -0,0 +1,73 @@
# Copying
Copyright (c) 2020 Proton Technologies AG
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.
# Dependencies
ProtonMail Bridge app includes the following libraries from Proton Technologies AG:
* [GopenPGP library](https://gopenpgp.org/) | The [MIT License](https://github.com/ProtonMail/gopenpgp/blob/master/LICENSE).
ProtonMail Bridge includes the following 3rd party software:
* [The Go Project libraries](https://golang.org/project/) | Available under [BSD license](https://golang.org/LICENSE)
* [Qt Go binding](https://github.com/therecipe/qt) | Available under [LGPLv3 license](https://github.com/therecipe/qt/blob/master/LICENSE)
* [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing)
* [Font Awesome 4.7.0](https://fontawesome.com/v4.7.0/) | Available under [multiple licenses](https://fontawesome.com/v4.7.0/license/)
* [notificator](github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
* [ishell](github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
* [readline](github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
* [singleinstance](github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
* [cascadia](github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
* [gocertifi](github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE)
* [logex](github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
* [test](github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
* [godog](github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
* [wincred](github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
* [credential-helpers](github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
* [imap](github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
* [imap-appendlimit](github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
* [imap-idle](github.com/emersion/go-imap-idle) | Available under [license](https://github.com/emersion/go-imap-idle/blob/master/LICENSE)
* [imap-quota](github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
* [imap-specialuse](github.com/emersion/go-imap-specialuse) | Available under [license](https://github.com/emersion/go-imap-specialuse/blob/master/LICENSE)
* [sasl](github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
* [smtp](github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
* [textwrapper](github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [vcard](github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [color](github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE)
* [shlex](github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
* [raven](github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE)
* [resty](github.com/go-resty/resty/v2) | Available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
* [mock](github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE)
* [cmp](github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
* [gopherjs](github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE)
* [multierror](github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
* [bcrypt](github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
* [html2text](github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [enmime](github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE)
* [osext](github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE)
* [keychain](github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [aurora](github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
* [dns](github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [uuid](github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE)
* [jsondiff](github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
* [logrus](github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
* [golang](github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE)
* [testify](github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [uuid](github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE)
* [cli](github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE)
* [BBolt](https://pkg.go.dev/go.etcd.io/bbolt/?tab=doc) | Available under [license](https://pkg.go.dev/go.etcd.io/bbolt?tab=licenses#LICENSE)
* [testify.v1](https://gopkg.in/stretchr/testify.v1) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)

View File

@ -1,72 +0,0 @@
# Copying
Copyright (c) 2020 Proton Technologies AG
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.
# Dependencies
ProtonMail Bridge app includes the following libraries from Proton Technologies AG:
* [GopenPGP library](https://gopenpgp.org/) | The [MIT License](https://github.com/ProtonMail/gopenpgp/blob/master/LICENSE).
ProtonMail Bridge includes the following 3rd party software:
* [The Go Project libraries](https://golang.org/project/) | Available under [BSD license](https://golang.org/LICENSE)
* [Qt Go binding](https://github.com/therecipe/qt) | Available under [LGPLv3 license](https://github.com/therecipe/qt/blob/master/LICENSE)
* [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing)
* [Font Awesome 4.7.0](https://fontawesome.com/v4.7.0/) | Available under [multiple licenses](https://fontawesome.com/v4.7.0/license/)
* [notificator](https://github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
* [ishell](https://github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
* [readline](https://github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
* [singleinstance](https://github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
* [cascadia](https://github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
* [gocertifi](https://github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE)
* [logex](https://github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
* [test](https://github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
* [godog](https://github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
* [wincred](https://github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
* [credential-helpers](https://github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
* [imap](https://github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
* [imap-appendlimit](https://github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
* [imap-idle](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)
* [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)
* [vcard](https://github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [color](https://github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE.md)
* [shlex](https://github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/COPYING)
* [raven](https://github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE)
* [resty](https://github.com/go-resty/resty) | Available under [license](https://github.com/go-resty/resty/blob/master/LICENSE)
* [mock](https://github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE)
* [cmp](https://github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
* [gopherjs](https://github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE)
* [multierror](https://github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
* [bcrypt](https://github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
* [html2text](https://github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [enmime](https://github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE)
* [osext](https://github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE)
* [keychain](https://github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [aurora](https://github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
* [dns](https://github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [uuid](https://github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE)
* [jsondiff](https://github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
* [logrus](https://github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
* [golang](https://github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE)
* [testify](https://github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [uuid](https://github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE)
* [cli](https://github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE)
* [BBolt](https://pkg.go.dev/go.etcd.io/bbolt/?tab=doc) | Available under [license](https://pkg.go.dev/go.etcd.io/bbolt?tab=licenses#LICENSE)
* [testify.v1](https://gopkg.in/stretchr/testify.v1) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)

File diff suppressed because it is too large Load Diff

281
Makefile
View File

@ -1,175 +1,88 @@
export GO111MODULE=on export GO111MODULE=on
# By default, the target OS is the same as the host OS,
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
GOOS:=$(shell go env GOOS) GOOS:=$(shell go env GOOS)
TARGET_CMD?=Desktop-Bridge
TARGET_OS?=${GOOS}
## Build ## Build
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher .PHONY: build check-has-go
# Keep version hardcoded so app build works also without Git repository. VERSION?=1.2.6-git
BRIDGE_APP_VERSION?=1.7.0+git
IE_APP_VERSION?=1.3.3+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
WINDRES_DEFINE:=BUILD_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
WINDRES_DEFINE:=BUILD_IE
endif
REVISION:=$(shell git rev-parse --short=10 HEAD) REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z) BUILD_TIME:=$(shell date +%FT%T%z)
BUILD_TAGS?=pmapi_prod
BUILD_FLAGS:=-tags='${BUILD_TAGS}' BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS} BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
BUILD_FLAGS_GUI:=-tags='${BUILD_TAGS} build_qt' GO_LDFLAGS:=$(addprefix -X main.,Version=${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}" "" ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+= ${BUILD_LDFLAGS} GO_LDFLAGS+= ${BUILD_LDFLAGS}
endif endif
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS} GO_LDFLAGS:=-ldflags '${GO_LDFLAGS}'
GO_LDFLAGS_LAUNCHER+=$(addprefix -X main.,ConfigName=${CONFIGNAME} ExeName=proton-${APP}) BUILD_FLAGS+= ${GO_LDFLAGS}
ifeq "${TARGET_OS}" "windows" BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
GO_LDFLAGS_LAUNCHER+=-H=windowsgui
endif
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}' DEPLOY_DIR:=cmd/Desktop-Bridge/deploy
BUILD_FLAGS_GUI+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
ICO_FILES:= ICO_FILES:=
DIRNAME:=$(shell basename ${CURDIR}) EXE:=$(shell basename ${CURDIR})
EXE:=${EXE_NAME}
EXE_QT:=${DIRNAME} ifeq "${GOOS}" "windows"
ifeq "${TARGET_OS}" "windows" EXE+=.exe
EXE:=${EXE}.exe ICO_FILES:=logo.ico icon.rc icon_windows.syso
EXE_QT:=${EXE_QT}.exe
RESOURCE_FILE:=resource.syso
endif endif
ifeq "${TARGET_OS}" "darwin" ifeq "${GOOS}" "darwin"
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
EXE:=${EXE}.app EXE:=${EXE}.app/Contents/MacOS/${EXE}
EXE_QT:=${EXE_QT}.app
EXE_BINARY_DARWIN:=/Contents/MacOS/${EXE_NAME}
endif
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
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 endif
EXE_TARGET:=${DEPLOY_DIR}/${GOOS}/${EXE}
TGZ_TARGET:=bridge_${GOOS}_${REVISION}.tgz
build: ${TGZ_TARGET} build: ${TGZ_TARGET}
build-ie: ${TGZ_TARGET}: ${DEPLOY_DIR}/${GOOS}
TARGET_CMD=Import-Export $(MAKE) build
build-nogui: gofiles
go build ${BUILD_FLAGS} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui
ifeq "${GOOS}" "windows"
PRERESOURCECMD:=cp ./resource.syso ./cmd/launcher/resource.syso
POSTRESOURCECMD:=rm -f ./cmd/launcher/resource.syso
endif
build-launcher: ${RESOURCE_FILE}
${PRERESOURCECMD}
go build ${BUILD_FLAGS_LAUNCHER} -o launcher-${EXE} ./cmd/launcher/
${POSTRESOURCECMD}
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 $@ rm -f $@
cd ${DEPLOY_DIR}/${TARGET_OS} && tar czf ../../../../$@ . cd ${DEPLOY_DIR} && tar czf ../../../$@ ${GOOS}
${DEPLOY_DIR}/linux: ${EXE_TARGET} ${DEPLOY_DIR}/linux: ${EXE_TARGET}
cp -pf ./internal/frontend/share/icons/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg cp -pf ./internal/frontend/share/icons/logo.svg ${DEPLOY_DIR}/linux/
cp -pf ./LICENSE ${DEPLOY_DIR}/linux/ cp -pf ./LICENSE ${DEPLOY_DIR}/linux/
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/ cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
cp -pf ./dist/${EXE_NAME}.desktop ${DEPLOY_DIR}/linux/
${DEPLOY_DIR}/darwin: ${EXE_TARGET} ${DEPLOY_DIR}/darwin: ${EXE_TARGET}
if [ "${DIRNAME}" != "${EXE_NAME}" ]; then \ cp ./internal/frontend/share/icons/Bridge.icns ${DARWINAPP_CONTENTS}/Resources/
mv ${EXE_TARGET}/Contents/MacOS/{${DIRNAME},${EXE_NAME}}; \ cp -r "utils/addcert.scpt" ${DARWINAPP_CONTENTS}/Resources/
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/ cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}"
${DEPLOY_DIR}/windows: ${EXE_TARGET} ${DEPLOY_DIR}/windows: ${EXE_TARGET}
cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico cp ./internal/frontend/share/icons/logo.ico ${DEPLOY_DIR}/windows/
cp LICENSE ${DEPLOY_DIR}/windows/
QT_BUILD_TARGET:=build desktop ${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
ifneq "${GOOS}" "${TARGET_OS}" rm -rf deploy ${GOOS} ${DEPLOY_DIR}
ifeq "${TARGET_OS}" "windows" cp cmd/Desktop-Bridge/main.go .
QT_BUILD_TARGET:=-docker build windows_64_shared qtdeploy ${BUILD_FLAGS} build desktop
endif mv deploy cmd/Desktop-Bridge
endif rm -rf ${GOOS} main.go
${EXE_TARGET}: check-has-go gofiles ${RESOURCE_FILE} ${VENDOR_TARGET} logo.ico: ./internal/frontend/share/icons/logo.ico
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR} cp $^ .
cp cmd/${TARGET_CMD}/main.go . icon.rc: ./internal/frontend/share/icon.rc
qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET} cp $^ .
mv deploy cmd/${TARGET_CMD} ./internal/frontend/qt/icon_windows.syso: ./internal/frontend/share/icon.rc logo.ico
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi windres $< $@
rm -rf ${TARGET_OS} main.go icon_windows.syso: ./internal/frontend/qt/icon_windows.syso
cp $^ .
WINDRES_YEAR:=$(shell date +%Y)
APP_VERSION_COMMA:=$(shell echo "${APP_VERSION}" | sed -e 's/[^0-9,.]*//g' -e 's/\./,/g')
resource.syso: ./internal/frontend/share/info.rc ./internal/frontend/share/icons/${SRC_ICO} .FORCE
rm -f ./*.syso
windres --target=pe-x86-64 -I ./internal/frontend/share/icons/ -D ${WINDRES_DEFINE} -D ICO_FILE=${SRC_ICO} -D EXE_NAME="${EXE_NAME}" -D FILE_VERSION="${APP_VERSION}" -D ORIGINAL_FILE_NAME="${EXE}" -D PRODUCT_VERSION="${APP_VERSION}" -D FILE_VERSION_COMMA=${APP_VERSION_COMMA} -D YEAR=${WINDRES_YEAR} -o $@ $<
## Rules for therecipe/qt ## Rules for therecipe/qt
.PHONY: prepare-vendor update-vendor update-qt-docs .PHONY: prepare-vendor update-vendor
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513 THERECIPE_QTVER:=$(shell grep "github.com/therecipe/qt " go.mod | sed -r 's;.* v[0-9\.]+-[0-9]+-([a-f0-9]*).*;\1;')
THERECIPE_ENV:=github.com/therecipe/env_${GOOS}_amd64_513
# vendor folder will be deleted by gomod hence we cache the big repo # vendor folder will be deleted by gomod hence we cache the big repo
# therecipe/env in order to download it only once # therecipe/env in order to download it only once
vendor-cache/${THERECIPE_ENV}: vendor-cache/${THERECIPE_ENV}:
git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV} git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV}
if [ "${TARGET_OS}" == "darwin" ]; then cp -f "./utils/QTBUG-88600/libqcocoa.dylib" "./vendor-cache/${THERECIPE_ENV}/5.13.0/clang_64/plugins/platforms/"; fi;
# The command used to make symlinks is different on windows.
# So if the GOOS is windows and we aren't crossbuilding (in which case the host os would still be *nix)
# we need to change the LINKCMD to something windowsy.
LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV} LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV}
ifeq "${GOOS}" "windows" ifeq "${GOOS}" "windows"
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV} WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
@ -184,12 +97,10 @@ prepare-vendor:
update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
${LINKCMD} ${LINKCMD}
update-qt-docs:
go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API)
## Dev dependencies ## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks .PHONY: install-devel-tools install-linter install-go-mod-outdated
LINTVER:="v1.39.0" LINTVER:="v1.23.6"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh" LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -205,44 +116,27 @@ install-linter: check-has-go
install-go-mod-outdated: install-go-mod-outdated:
which go-mod-outdated || go get -u github.com/psampaz/go-mod-outdated which go-mod-outdated || go get -u github.com/psampaz/go-mod-outdated
install-git-hooks:
cp utils/githooks/* .git/hooks/
chmod +x .git/hooks/*
## Checks, mocks and docs ## Checks, mocks and docs
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc release-notes .PHONY: check-has-go check-license test bench coverage mocks lint updates doc
check-has-go: check-has-go:
@which go || (echo "Install Go-lang!" && exit 1) @which go || (echo "Install Go-lang!" && exit 1)
add-license: check-license:
./utils/missing_license.sh add find . -not -path "./vendor/*" -not -name "*mock*.go" -regextype posix-egrep -regex ".*\.go|.*\.qml" -exec grep -L "Copyright (c) 2020 Proton Technologies AG" {} \;
change-copyright-year:
./utils/missing_license.sh change-year
test: gofiles test: gofiles
@# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests. @# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests.
go test -coverprofile=/tmp/coverage.out -run=${TESTRUN} \ go test -coverprofile=/tmp/coverage.out -run=${TESTRUN} \
./internal/api/... \ ./internal/api/... \
./internal/bridge/... \ ./internal/bridge/... \
./internal/config/... \
./internal/constants/... \
./internal/cookies/... \
./internal/crash/... \
./internal/events/... \ ./internal/events/... \
./internal/frontend/autoconfig/... \ ./internal/frontend/autoconfig/... \
./internal/frontend/cli/... \ ./internal/frontend/cli/... \
./internal/imap/... \ ./internal/imap/... \
./internal/importexport/... \ ./internal/preferences/... \
./internal/locations/... \
./internal/logging/... \
./internal/metrics/... \
./internal/smtp/... \ ./internal/smtp/... \
./internal/store/... \ ./internal/store/... \
./internal/transfer/... \
./internal/updater/... \
./internal/users/... \
./internal/versioner/... \
./pkg/... ./pkg/...
bench: bench:
@ -254,24 +148,12 @@ coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks: mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/bridge Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer > internal/bridge/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/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/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/message Fetcher > pkg/message/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-changelog lint:
lint-license:
./utils/missing_license.sh check
lint-changelog:
./utils/changelog_linter.sh Changelog.md
lint-golang:
which golangci-lint || $(MAKE) install-linter which golangci-lint || $(MAKE) install-linter
$(info linting with GOMAXPROCS=${GOMAXPROCS})
golangci-lint run ./... golangci-lint run ./...
updates: install-go-mod-outdated updates: install-go-mod-outdated
@ -281,29 +163,20 @@ updates: install-go-mod-outdated
doc: doc:
godoc -http=:6060 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 .PHONY: gofiles
# Following files are for the whole app so it makes sense to have them in bridge package. # Following files are for the whole app so it makes sense to have them in bridge package.
# (Options like cmd or internal were considered and bridge package is the best place for them.) # (Options like cmd or internal were considered and bridge package is the best place for them.)
gofiles: ./internal/bridge/credits.go ./internal/importexport/credits.go gofiles: ./internal/bridge/credits.go ./internal/bridge/release_notes.go
./internal/bridge/credits.go: ./utils/credits.sh go.mod ./internal/bridge/credits.go: ./utils/credits.sh go.mod
cd ./utils/ && ./credits.sh bridge cd ./utils/ && ./credits.sh
./internal/importexport/credits.go: ./utils/credits.sh go.mod ./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes.txt ./release-notes/bugs.txt
cd ./utils/ && ./credits.sh importexport cd ./utils/ && ./release-notes.sh
## Run and debug ## Run and debug
.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 .PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug qmlpreview qt-fronted-clean clean
VERBOSITY?=debug-client
LOG?=debug RUN_FLAGS:=-m -l=${VERBOSITY}
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_FLAGS_IE?=-m -l=${LOG}
run: run-nogui-cli run: run-nogui-cli
@ -313,47 +186,23 @@ run-qt-cli: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
run-nogui: clean-vendor gofiles run-nogui: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} | tee last.log
run-nogui-cli: clean-vendor gofiles run-nogui-cli: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} -c
run-debug: run-debug:
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS} PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/Desktop-Bridge/main.go -- ${RUN_FLAGS}
run-qml-preview: run-qml-preview:
$(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview make -C internal/frontend/qt -f Makefile.local qmlpreview
run-ie-qml-preview:
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview
run-ie:
TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run
run-ie-qt:
TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-qt
run-ie-nogui:
TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-nogui
clean-frontend-qt: clean-frontend-qt:
$(MAKE) -C internal/frontend/qt -f Makefile.local clean make -C internal/frontend/qt -f Makefile.local clean
clean-frontend-qt-ie:
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local clean
clean-frontend-qt-common:
$(MAKE) -C internal/frontend/qt-common -f Makefile.local clean
clean-vendor: clean-frontend-qt clean-frontend-qt-ie clean-frontend-qt-common clean-vendor: clean-frontend-qt
rm -rf ./vendor rm -rf ./vendor
clean: clean-vendor clean: clean-frontend-qt
rm -rf vendor-cache rm -rf vendor-cache
rm -rf cmd/Desktop-Bridge/deploy rm -rf cmd/Desktop-Bridge/deploy
rm -rf cmd/Import-Export/deploy rm -f build last.log mem.pprof
rm -f build last.log mem.pprof main.go
rm -f resource.syso
rm -f release-notes/bridge.html
rm -f release-notes/import-export.html
.PHONY: generate
generate:
go generate ./...
$(MAKE) add-license
.FORCE:

View File

@ -1,13 +1,12 @@
# ProtonMail Bridge and Import Export app # ProtonMail Bridge
Copyright (c) 2020 Proton Technologies AG Copyright (c) 2020 Proton Technologies AG
This repository holds the ProtonMail Bridge and the ProtonMail Import-Export applications. This repository holds the ProtonMail Bridge application.
For a detailed build information see [BUILDS](./BUILDS.md). For a detailed build information see [BUILDS](./BUILDS.md).
The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md). For licensing information see [COPYING](./COPYING.md).
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md). For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
## Description
## Description Bridge
ProtonMail Bridge for e-mail clients. ProtonMail Bridge for e-mail clients.
When launched, Bridge will initialize local IMAP/SMTP servers and render When launched, Bridge will initialize local IMAP/SMTP servers and render
@ -24,30 +23,6 @@ background.
More details [on the public website](https://protonmail.com/bridge). More details [on the public website](https://protonmail.com/bridge).
## Description Import-Export app
ProtonMail Import-Export app for importing and exporting messages.
To transfer messages, firstly log in using your ProtonMail credentials.
For import, expand your account, and pick the address to which to import
messages from IMAP server or local EML or MBOX files. For export, pick
the whole account or only a specific address. Then, in both cases,
configure transfer rules (match source and target mailboxes, set time
range limits and so on) and hit start. Once the transfer is complete,
check the results.
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 ## Keychain
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
@ -56,53 +31,22 @@ Windows, Bridge uses native credential managers. On Linux, use
or or
[pass](https://www.passwordstore.org/). [pass](https://www.passwordstore.org/).
## Environment Variables ## Environment Variables
### Bridge application ### Bridge application
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable. - `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
### Dev build or run ### Dev build or run
- `APP_VERSION`: set the bridge app version used during testing or building
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes - `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
- `VERBOSITY`: set log level used during test time and by the makefile - `VERBOSITY`: set log level used during test time and by the makefile.
- `VERSION`: set the bridge app version used during testing or building.
### Integration testing ### Integration testing
- `TEST_ENV`: set which env to use (fake or live) - `TEST_ENV`: set which env to use (fake or live)
- `TEST_APP`: set which app to test (bridge or ie)
- `TEST_ACCOUNTS`: set JSON file with configured accounts - `TEST_ACCOUNTS`: set JSON file with configured accounts
- `TAGS`: set build tags for tests - `TAGS`: set build tags for tests
- `FEATURES`: set feature dir, file or scenario to test - `FEATURES`: set feature dir, file or scenario to test
## Files
### Database
The database stores metadata necessary for presenting messages and mailboxes to an email client:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
### Preferences
User preferences are stored in json at the following location:
- Linux: `~/.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:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
### Lock file
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
### TLS Certificate and Key
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`

4
ci/Dockerfile Normal file
View File

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

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -35,40 +35,400 @@ package main
*/ */
import ( import (
"fmt"
"io/ioutil"
"os" "os"
"os/exec"
"path/filepath"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/app/base" "github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/app/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"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/pmapifactory"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/pkg/args"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/allan-simon/go-singleinstance"
"github.com/getsentry/raven-go"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
const ( // cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
appName = "ProtonMail Bridge" // Different number will drop old files and create new ones.
appUsage = "ProtonMail IMAP and SMTP Bridge" const cacheVersion = "c11"
configName = "bridge"
updateURLName = "bridge" // Following variables are set via ldflags during build.
keychainName = "bridge" var (
cacheVersion = "c11" // Version of the build.
Version = "" //nolint[gochecknoglobals]
// Revision is current hash of the build.
Revision = "" //nolint[gochecknoglobals]
// BuildTime stamp of the build.
BuildTime = "" //nolint[gochecknoglobals]
// AppShortName to make setup
AppShortName = "bridge" //nolint[gochecknoglobals]
// DSNSentry client keys to be able to report crashes to Sentry
DSNSentry = "" //nolint[gochecknoglobals]
)
var (
longVersion = Version + " (" + Revision + ")" //nolint[gochecknoglobals]
buildVersion = longVersion + " " + BuildTime //nolint[gochecknoglobals]
log = config.GetLogEntry("main") //nolint[gochecknoglobals]
// How many crashes in a row.
numberOfCrashes = 0 //nolint[gochecknoglobals]
// After how many crashes bridge gives up starting.
maxAllowedCrashes = 10 //nolint[gochecknoglobals]
) )
func main() { func main() {
base, err := base.New( if err := raven.SetDSN(DSNSentry); err != nil {
appName, log.WithError(err).Errorln("Can not setup sentry DSN")
appUsage,
configName,
updateURLName,
keychainName,
cacheVersion,
)
if err != nil {
logrus.WithError(err).Fatal("Failed to create app base")
} }
// Other instance already running. raven.SetRelease(Revision)
if base == nil { bridge.UpdateCurrentUserAgent(Version, runtime.GOOS, "", "")
args.FilterProcessSerialNumberFromArgs()
filterRestartNumberFromArgs()
app := cli.NewApp()
app.Name = "Protonmail Bridge"
app.Version = 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", Version).
WithField("revision", Revision).
WithField("runtime", runtime.GOOS).
WithField("build", BuildTime).
WithField("args", os.Args).
WithField("appLong", app.Name).
WithField("appShort", AppShortName).
Info("Run app")
if err := app.Run(os.Args); err != nil {
log.Error("Program exited with error: ", err)
}
}
type panicHandler struct {
cfg *config.Config
err *error // Pointer to error of cli action.
}
func (ph *panicHandler) HandlePanic() {
r := recover()
if r == nil {
return return
} }
if err := bridge.New(base).Run(os.Args); err != nil { config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r))
logrus.WithError(err).Fatal("Bridge exited with error") 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(AppShortName, Version, 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(AppShortName, Version, Revision, BuildTime, bridge.ReleaseNotes, bridge.ReleaseFixedBugs, cfg.GetUpdateDir())
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")
}
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()
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
pmapiClientFactory := pmapifactory.New(cfg, eventListener)
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, Version, pmapiClientFactory, 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(Version, 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(AppShortName, Version, Revision, "c10").GetPreferencesPath()
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
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, 0644)
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)
}
} }
} }

View File

@ -1,57 +0,0 @@
// 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")
}
}

View File

@ -1,194 +0,0 @@
// 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/config/useragent"
"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/sentry"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/internal/versioner"
"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, useragent.New())
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))
}

View File

@ -1,11 +0,0 @@
[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

View File

@ -1,11 +0,0 @@
[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

View File

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

View File

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

90
go.mod
View File

@ -5,70 +5,74 @@ go 1.13
// These dependencies are `replace`d below, so the version numbers should be ignored. // These dependencies are `replace`d below, so the version numbers should be ignored.
// They are in a separate require block to highlight this. // They are in a separate require block to highlight this.
require ( require (
github.com/docker/docker-credential-helpers v0.6.3 github.com/docker/docker-credential-helpers v0.0.0-00010101000000-000000000000
github.com/emersion/go-imap v1.0.6 github.com/emersion/go-imap v0.0.0-20171113213225-939ec3994dbe
github.com/emersion/go-imap-quota v0.0.0-20171113212021-e883a2bc54d6
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
) )
require ( require (
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
github.com/Masterminds/semver/v3 v3.1.0 github.com/ProtonMail/go-appdir v1.0.0
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee
github.com/ProtonMail/go-rfc5322 v0.5.0
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp/v2 v2.1.3 github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed
github.com/PuerkitoBio/goquery v1.5.1
github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
github.com/andybalholm/cascadia v1.1.0
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cucumber/godog v0.8.1 github.com/cucumber/godog v0.8.1
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a github.com/danieljoos/wincred v1.0.2 // indirect
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c 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-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe
github.com/emersion/go-mbox v1.0.2 github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
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/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
github.com/fatih/color v1.9.0 github.com/fatih/color v1.9.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/getsentry/sentry-go v0.8.0 github.com/getsentry/raven-go v0.2.0
github.com/go-resty/resty/v2 v2.3.0 github.com/go-resty/resty/v2 v2.2.0
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.3
github.com/google/go-cmp v0.5.1 github.com/google/go-cmp v0.4.0
github.com/google/uuid v1.1.1
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-multierror v1.0.0
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d github.com/jhillyerd/enmime v0.8.0
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2
github.com/miekg/dns v1.1.30 github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/miekg/dns v1.1.29
github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/myesui/uuid v1.0.0 // indirect
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.4.2
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/stretchr/testify v1.5.1
github.com/stretchr/testify v1.6.1 github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
github.com/urfave/cli/v2 v2.2.0 github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
github.com/vmihailenco/msgpack/v5 v5.1.3 github.com/twinj/uuid v1.0.0 // indirect
go.etcd.io/bbolt v1.3.5 github.com/urfave/cli v1.22.3
golang.org/x/net v0.0.0-20200707034311-ab3426394381 go.etcd.io/bbolt v1.3.3
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
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
) )
replace ( replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.0.0
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f
github.com/emersion/go-imap-quota => github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f
) )

387
go.sum
View File

@ -1,253 +1,167 @@
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/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/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 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c h1:iaVbEOnskSGgcH7XQWHG6VPirHDRoYe+Idd0/dl4m8A= github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f h1:cFhATQTJGK2iZ0dc+jRhr75mh6bsc5Ug6NliaBya8Kw=
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.0.0 h1:0DQXbZNvUszWgXUuP7TzvQdwnkK1D5Zf/glBgCFJFCk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.0.0/go.mod h1:R1gQindzdYFcWJuuGXteYHDJzUCVtyU+EpEqp9aWcFs=
github.com/ProtonMail/go-appdir v1.0.0 h1:PZXQ0HkveuEugga3LeDycxWtybrXQfKR0ThxURd6ojw=
github.com/ProtonMail/go-appdir v1.0.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
github.com/ProtonMail/go-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 h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2 h1:pQkjJELHayW59jp7r4G5Dlmnicr5McejDfwsjcwI1SU= github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f h1:QkLm4yfhBQuBxrC46Vhy2sonOWVrwIJo5bgKpA82+TY=
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2/go.mod h1:HTM9X7e9oLwn7RiqLG0UVwVRJenLs3wN+tQ0NPAfwMQ= github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f/go.mod h1:+m2uLXghuYktgE/vc5AkmCxx1qhu33ZKHFWg1cGZPD0=
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs= github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee h1:Q/nK7A9xzUimAZsQDa/yaw3xW9PkTTnJnkT5wAkXrmI=
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-20171219160728-ed0baee567ee/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw= github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de h1:+LA9teDYUwGkBvg0kqZPZetmxIv1r7s9/npBP1yzKs0=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de/go.mod h1:85zbnYVWIY7//iScX9fnB/kKOGH9B86YPqtpr7f1i7A=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4= github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72 h1:hGCc4Oc2fD3I5mNnZ1VlREncVc9EXJF8dxW3sw16gWM=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-rfc5322 v0.5.0 h1:LbKWjgfvumYZCr8BgGyTUk3ETGkFLAjQdkuSUpZ5CcE= github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
github.com/ProtonMail/go-rfc5322 v0.5.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU= github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp/v2 v2.1.3 h1:4+nFDJ9WtcUQTip/je2Ll3P21XhAUl4asWsafLrw97c= github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed h1:3gib6hGF61VfRu7cqqkODyRUgES5uF/fkLQanPPJiO8=
github.com/ProtonMail/gopenpgp/v2 v2.1.3/go.mod h1:WeYndoqEcRR4/QbgRL24z6OwYX5T1RWerRk8NfZ6rJM= github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed/go.mod h1:NstNbZx1OIoyq+2qHAFLwDFpHbMk8L2i2Vr+LioJ3/g=
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 h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= 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 h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U= github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY= github.com/aslakhellesoy/gox v1.0.100/go.mod h1:AJl542QsKKG96COVsv0N74HHzVQgDIQPceVUh1aeU2M=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 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/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 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/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 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 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 h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cucumber/gherkin-go/v11 v11.0.0 h1:cwVwN1Qn2VRSfHZNLEh5x00tPBmZcjATBWDpxsR5Xug=
github.com/cucumber/gherkin-go/v11 v11.0.0/go.mod h1:CX33k2XU2qog4e+TFjOValoq6mIUq0DmVccZs238R9w=
github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8= github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8=
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g= github.com/cucumber/godog v0.9.0 h1:QOb8wyC7f+FVFXzY3RdgowwJUb4WeJfqbnQqaH4jp+A=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/cucumber/godog v0.9.0/go.mod h1:roWCHkpeK6UTOyIRRl7IR+fgfBeZ4vZR7OSq2J/NbM4=
github.com/cucumber/messages-go/v10 v10.0.1/go.mod h1:kA5T38CBlBbYLU12TIrJ4fk4wSkVVOgyh7Enyy8WnSg=
github.com/cucumber/messages-go/v10 v10.0.3 h1:m/9SD/K/A15WP7i1aemIv7cwvUw+viS51Ui5HBw1cdE=
github.com/cucumber/messages-go/v10 v10.0.3/go.mod h1:9jMZ2Y8ZxjLY6TG2+x344nt5rXstVVDYSdS5ySfI1WY=
github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42 h1:3TeZ5gy3We/LVL0sqmGhM8dFDTSM7Hyj7PMIdl6OTs4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89 h1:AzbVhcrxgJO5MfSvzG5q4IfrYVm0Jw4AHNPz47+DiR0=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc= github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ= github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe h1:2R2XpJkmbyy7PcSjnCPOnNfu+GuRzgWR9U2+j/d1O+0=
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po= github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78= github.com/emersion/go-imap-unselect v0.0.0-20161227193600-1e6dc73ac8fe h1:WeXweyFnbM2DQx0wxHkJKXYXwXpApopIeAjDTipW5Z4=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0= github.com/emersion/go-imap-unselect v0.0.0-20161227193600-1e6dc73ac8fe/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
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-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I= github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
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-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-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
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-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 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI= 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 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 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 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/getsentry/sentry-go v0.8.0 h1:F52cjBVLuiTfdW6p4JFuxlt3pOjKfWYT/aka7cdJ7v0= github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
github.com/getsentry/sentry-go v0.8.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
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.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-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/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 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195 h1:j0UEFmS7wSjAwKEIkgKBn8PRDfjcuggzr93R9wk53nQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/jhillyerd/enmime v0.8.0 h1:PHc/2LXtnDmCDm0V4+5NlBx+MoubmufhuNXwpKSV2o8=
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2 h1:1XZArHAPddaXKbg51etNbCjkNUkKgSa0s8dSz2LYB2g=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
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.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
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 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.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 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo= github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
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/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-20190712045011-8443391ee9b6 h1:qsqscDgSJy+HqgMTR+3NwjYJBbp1+honwDsszLoS+pA=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6/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.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
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 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.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 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/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 h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= 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= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -257,111 +171,66 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk= github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
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.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-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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-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-20190620200207-3b0461eec859/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-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-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-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-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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
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-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.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-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-20181030221726-6c7e314b6563/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-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
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= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -22,32 +22,39 @@
package api package api
import ( import (
"crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"github.com/ProtonMail/proton-bridge/internal/bridge" "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/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/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/sirupsen/logrus"
) )
var ( var (
log = logrus.WithField("pkg", "api") //nolint[gochecknoglobals] log = config.GetLogEntry("api") //nolint[gochecknoglobals]
) )
type apiServer struct { type apiServer struct {
host string host string
settings *settings.Settings pref *config.Preferences
tls *tls.Config
certPath string
keyPath string
eventListener listener.Listener eventListener listener.Listener
} }
// NewAPIServer returns prepared API server struct. // NewAPIServer returns prepared API server struct.
func NewAPIServer(settings *settings.Settings, eventListener listener.Listener) *apiServer { //nolint[golint] func NewAPIServer(pref *config.Preferences, tls *tls.Config, certPath, keyPath string, eventListener listener.Listener) *apiServer { //nolint[golint]
return &apiServer{ return &apiServer{
host: bridge.Host, host: bridge.Host,
settings: settings, pref: pref,
tls: tls,
certPath: certPath,
keyPath: keyPath,
eventListener: eventListener, eventListener: eventListener,
} }
} }
@ -61,10 +68,12 @@ func (api *apiServer) ListenAndServe() {
server := &http.Server{ server := &http.Server{
Addr: addr, Addr: addr,
Handler: mux, Handler: mux,
TLSConfig: api.tls,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
} }
log.Info("API listening at ", addr) log.Info("API listening at ", addr)
if err := server.ListenAndServe(); err != nil { if err := server.ListenAndServeTLS(api.certPath, api.keyPath); err != nil {
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error()) api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
log.Error("API failed: ", err) log.Error("API failed: ", err)
} }
@ -72,10 +81,10 @@ func (api *apiServer) ListenAndServe() {
} }
func (api *apiServer) getAddress() string { func (api *apiServer) getAddress() string {
port := api.settings.GetInt(settings.APIPortKey) port := api.pref.GetInt(preferences.APIPortKey)
newPort := ports.FindFreePortFrom(port) newPort := ports.FindFreePortFrom(port)
if newPort != port { if newPort != port {
api.settings.SetInt(settings.APIPortKey, newPort) api.pref.SetInt(preferences.APIPortKey, newPort)
} }
return getAPIAddress(api.host, newPort) return getAPIAddress(api.host, newPort)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -18,6 +18,7 @@
package api package api
import ( import (
"crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
@ -36,9 +37,12 @@ func focusHandler(ctx handlerContext) error {
// CheckOtherInstanceAndFocus is helper for new instances to check if there is // CheckOtherInstanceAndFocus is helper for new instances to check if there is
// already a running instance and get it's focus. // already a running instance and get it's focus.
func CheckOtherInstanceAndFocus(port int) error { func CheckOtherInstanceAndFocus(port int, tls *tls.Config) error {
transport := &http.Transport{TLSClientConfig: tls}
client := &http.Client{Transport: transport}
addr := getAPIAddress(bridge.Host, port) addr := getAPIAddress(bridge.Host, port)
resp, err := (&http.Client{}).Get("http://" + addr + "/focus") resp, err := client.Get("https://" + addr + "/focus")
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,387 +0,0 @@
// 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/config/useragent"
"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/sentry"
"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/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
const (
flagCPUProfile = "cpu-prof"
flagCPUProfileShort = "p"
flagMemProfile = "mem-prof"
flagMemProfileShort = "m"
flagLogLevel = "log-level"
flagLogLevelShort = "l"
// FlagCLI indicate to start with command line interface.
FlagCLI = "cli"
flagCLIShort = "c"
flagRestart = "restart"
flagLauncher = "launcher"
)
type Base struct {
SentryReporter *sentry.Reporter
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
UserAgent *useragent.UserAgent
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) {
userAgent := useragent.New()
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
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
}
cm := pmapi.NewClientManager(getAPIConfig(configName, listener), userAgent)
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
cm.SetCookieJar(jar)
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{
SentryReporter: sentryReporter,
CrashHandler: crashHandler,
Locations: locations,
Settings: settingsObj,
Lock: lock,
Cache: cache,
Listener: listener,
Creds: credentials.NewStore(kc),
CM: cm,
CookieJar: jar,
UserAgent: userAgent,
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: flagCPUProfile,
Aliases: []string{flagCPUProfileShort},
Usage: "Generate CPU profile",
},
&cli.BoolFlag{
Name: flagMemProfile,
Aliases: []string{flagMemProfileShort},
Usage: "Generate memory profile",
},
&cli.StringFlag{
Name: flagLogLevel,
Aliases: []string{flagLogLevelShort},
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
},
&cli.BoolFlag{
Name: FlagCLI,
Aliases: []string{flagCLIShort},
Usage: "Use command line interface",
},
&cli.StringFlag{
Name: flagRestart,
Usage: "The number of times the application has already restarted",
Hidden: true,
},
&cli.StringFlag{
Name: flagLauncher,
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(flagLauncher); launcher != "" {
b.Autostart.Exec = []string{launcher}
b.command = launcher
}
if c.Bool(flagCPUProfile) {
startCPUProfile()
defer pprof.StopCPUProfile()
}
if c.Bool(flagMemProfile) {
defer makeMemoryProfile()
}
logging.SetLevel(c.String(flagLogLevel))
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(flagRestart) > 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
}
func getAPIConfig(configName string, listener listener.Listener) *pmapi.ClientConfig {
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, "") }
return apiConfig
}

View File

@ -1,131 +0,0 @@
// 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 1.5.x | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
// | c11 1.6.x | ~/.cache/protonmail/<app>/cache/c11 | ~/.config/protonmail/<app>/cache/c11 |
// | updates | ~/.cache/protonmail/<app>/updates | ~/.config/protonmail/<app>/updates |.
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()
if err := migratePrefsFrom15x(locations, userCacheDir); err != nil {
return err
}
if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil {
return err
}
if err := migrateUpdatesFrom16x(configName, locations); err != nil { //nolint[revive] It is more clear to structure this way
return err
}
return nil
}
func migratePrefsFrom15x(locations *locations.Locations, userCacheDir string) error {
newSettingsDir, err := locations.ProvideSettingsPath()
if err != nil {
return err
}
return moveIfExists(
filepath.Join(userCacheDir, "c11", "prefs.json"),
filepath.Join(newSettingsDir, "prefs.json"),
)
}
func migrateCacheFromBoth15xAnd16x(locations *locations.Locations, userCacheDir string) error {
olderCacheDir := userCacheDir
newerCacheDir := locations.GetOldCachePath()
latestCacheDir, err := locations.ProvideCachePath()
if err != nil {
return err
}
// Migration for versions before 1.6.x.
if err := moveIfExists(
filepath.Join(olderCacheDir, "c11"),
filepath.Join(latestCacheDir, "c11"),
); err != nil {
return err
}
// Migration for versions 1.6.x.
return moveIfExists(
filepath.Join(newerCacheDir, "c11"),
filepath.Join(latestCacheDir, "c11"),
)
}
func migrateUpdatesFrom16x(configName string, locations *locations.Locations) error {
// In order to properly update Bridge 1.6.X and higher we need to
// change the launcher first. Since this is not part of automatic
// updates the migration must wait until manual update. Until that
// we need to keep old path.
if configName == "bridge" {
return nil
}
oldUpdatesPath := locations.GetOldUpdatesPath()
// Do not use ProvideUpdatesPath, that creates dir right away.
newUpdatesPath := locations.GetUpdatesPath()
return moveIfExists(oldUpdatesPath, newUpdatesPath)
}
func moveIfExists(source, destination string) error {
l := logrus.WithField("source", source).WithField("destination", destination)
if _, err := os.Stat(source); os.IsNotExist(err) {
l.Info("No need to migrate file, source doesn't exist")
return nil
}
if _, err := os.Stat(destination); !os.IsNotExist(err) {
// Once migrated, files should not stay in source anymore. Therefore
// if some files are still in source location but target already exist,
// it's suspicious. Could happen by installing new version, then the
// old one because of some reason, and then the new one again.
// Good to see as warning because it could be a reason why Bridge is
// behaving weirdly, like wrong configuration, or db re-sync and so on.
l.Warn("No need to migrate file, target already exists")
return nil
}
l.Info("Migrating files")
return os.Rename(source, destination)
}

View File

@ -1,56 +0,0 @@
// 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()
}

View File

@ -1,79 +0,0 @@
// 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
}

View File

@ -1,49 +0,0 @@
// 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))
})
}
}

View File

@ -1,234 +0,0 @@
// 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"
)
const (
flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp"
flagNoWindow = "no-window"
flagNonInteractive = "noninteractive"
)
func New(base *base.Base) *cli.App {
app := base.NewApp(run)
app.Flags = append(app.Flags, []cli.Flag{
&cli.StringFlag{
Name: flagLogIMAP,
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"},
&cli.BoolFlag{
Name: flagLogSMTP,
Usage: "Enable logging of SMTP communications (may contain decrypted data!)"},
&cli.BoolFlag{
Name: flagNoWindow,
Usage: "Don't show window after start"},
&cli.BoolFlag{
Name: flagNonInteractive,
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.SentryReporter, 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(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
imapPort, tlsConfig, imapBackend, b.UserAgent, 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(flagLogSMTP),
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, "--"+flagNoWindow)
// 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(base.FlagCLI):
frontendMode = "cli"
case c.Bool(flagNonInteractive):
return <-(make(chan error)) // Block forever.
default:
frontendMode = "qt"
}
f := frontend.New(
constants.Version,
constants.BuildVersion,
b.Name,
frontendMode,
!c.Bool(flagNoWindow),
b.CrashHandler,
b.Locations,
b.Settings,
b.Listener,
b.Updater,
b.UserAgent,
bridge,
smtpBackend,
b.Autostart,
b,
)
// Watch for updates routine
go func() {
ticker := time.NewTicker(constants.UpdateCheckInterval)
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) {
log := logrus.WithField("pkg", "app/bridge")
version, err := u.Check()
if err != nil {
log.WithError(err).Error("An error occurred while checking for updates")
return
}
f.WaitUntilFrontendIsReady()
// Update links in UI
f.SetVersion(version)
if !u.IsUpdateApplicable(version) {
log.Info("No need to update")
return
}
log.WithField("version", version.Version).Info("An update is available")
if !autoUpdate {
f.NotifyManualUpdate(version, u.CanInstall(version))
return
}
if !u.CanInstall(version) {
log.Info("A manual update is required")
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
return
}
if err := u.InstallUpdate(version); err != nil {
if errors.Cause(err) == updater.ErrDownloadVerify {
log.WithError(err).Warning("Skipping update installation due to temporary error")
} else {
log.WithError(err).Error("The update couldn't be installed")
f.NotifySilentUpdateError(err)
}
return
}
f.NotifySilentUpdateInstalled()
}

View File

@ -1,110 +0,0 @@
// 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/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(base.FlagCLI):
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) { //nolint[unparam]
log := logrus.WithField("pkg", "app/ie")
version, err := u.Check()
if err != nil {
log.WithError(err).Error("An error occurred while checking for updates")
return
}
f.WaitUntilFrontendIsReady()
// Update links in UI
f.SetVersion(version)
if !u.IsUpdateApplicable(version) {
log.Info("No need to update")
return
}
log.WithField("version", version.Version).Info("An update is available")
f.NotifyManualUpdate(version, u.CanInstall(version))
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -15,76 +15,104 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package bridge provides core functionality of Bridge app. // Package bridge provides core business logic providing API over credentials store and PM API.
package bridge package bridge
import ( import (
"fmt" "errors"
"strconv" "strconv"
"strings"
"sync"
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/constants" m "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/sentry" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/hashicorp/go-multierror"
logrus "github.com/sirupsen/logrus" logrus "github.com/sirupsen/logrus"
) )
var ( var (
log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals] log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
isApplicationOutdated = false //nolint[gochecknoglobals]
) )
// Bridge is a struct handling users.
type Bridge struct { type Bridge struct {
*users.Users config Configer
pref PreferenceProvider
panicHandler PanicHandler
events listener.Listener
version string
pmapiClientFactory PMAPIProviderFactory
credStorer CredentialsStorer
storeCache *store.Cache
locations Locator // users is a list of accounts that have been added to bridge.
settings SettingsProvider // They are stored sorted in the credentials store in the order
clientManager users.ClientManager // that they were added to bridge chronologically.
updater Updater // People are used to that and so we preserve that ordering here.
versioner Versioner users []*User
// idleUpdates is a channel which the imap backend listens to and which it uses
// to send idle updates to the mail client (eg thunderbird).
// The user stores should send idle updates on this channel.
idleUpdates chan interface{}
lock sync.RWMutex
userAgentClientName string
userAgentClientVersion string
userAgentOS string
} }
func New( func New(
locations Locator, config Configer,
cache Cacher, pref PreferenceProvider,
s SettingsProvider, panicHandler PanicHandler,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, version string,
credStorer users.CredentialsStorer, pmapiClientFactory PMAPIProviderFactory,
updater Updater, credStorer CredentialsStorer,
versioner Versioner,
) *Bridge { ) *Bridge {
// Allow DoH before starting the app if the user has previously set this setting. log.Trace("Creating new bridge")
// This allows us to start even if protonmail is blocked.
if s.GetBool(settings.AllowProxyKey) {
clientManager.AllowProxy()
}
storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, clientManager, eventListener)
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
b := &Bridge{ b := &Bridge{
Users: u, config: config,
pref: pref,
locations: locations, panicHandler: panicHandler,
settings: s, events: eventListener,
clientManager: clientManager, version: version,
updater: updater, pmapiClientFactory: pmapiClientFactory,
versioner: versioner, credStorer: credStorer,
storeCache: store.NewCache(config.GetIMAPCachePath()),
idleUpdates: make(chan interface{}),
lock: sync.RWMutex{},
} }
if s.GetBool(settings.FirstStartKey) { // Allow DoH before starting bridge if the user has previously set this setting.
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil { // This allows us to start even if protonmail is blocked.
logrus.WithError(err).Error("Failed to send metric") if pref.GetBool(preferences.AllowProxyKey) {
AllowDoH()
} }
s.SetBool(settings.FirstStartKey, false) go func() {
defer panicHandler.HandlePanic()
b.watchBridgeOutdated()
}()
if b.credStorer == nil {
log.Error("Bridge has no credentials store")
} else if err := b.loadUsersFromCredentialsStore(); err != nil {
log.WithError(err).Error("Could not load all users from credentials store")
}
if pref.GetBool(preferences.FirstStartKey) {
b.SendMetric(m.New(m.Setup, m.FirstStart, m.Label(version)))
} }
go b.heartbeat() go b.heartbeat()
@ -94,98 +122,389 @@ func New(
// heartbeat sends a heartbeat signal once a day. // heartbeat sends a heartbeat signal once a day.
func (b *Bridge) heartbeat() { func (b *Bridge) heartbeat() {
for range time.Tick(time.Minute) { for range time.NewTicker(1 * time.Hour).C {
lastHeartbeatDay, err := strconv.ParseInt(b.settings.Get(settings.LastHeartbeatKey), 10, 64) next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
if err != nil { if err != nil {
continue continue
} }
nextTime := time.Unix(next, 0)
if time.Now().After(nextTime) {
b.SendMetric(m.New(m.Heartbeat, m.Daily, m.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. func (b *Bridge) loadUsersFromCredentialsStore() (err error) {
if time.Now().YearDay() == int(lastHeartbeatDay) { b.lock.Lock()
defer b.lock.Unlock()
userIDs, err := b.credStorer.List()
if err != nil {
return
}
for _, userID := range userIDs {
l := log.WithField("user", userID)
apiClient := b.pmapiClientFactory(userID)
user, newUserErr := newUser(b.panicHandler, userID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
if newUserErr != nil {
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
continue continue
} }
// We're on the next (or a different) day, so send a heartbeat. b.users = append(b.users, user)
if err := b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel)); err != nil {
logrus.WithError(err).Error("Failed to send heartbeat") if initUserErr := user.init(b.idleUpdates, apiClient); initUserErr != nil {
continue l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
}
} }
// Heartbeat was sent successfully so update the last heartbeat day. return err
b.settings.Set(settings.LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
} }
func (b *Bridge) watchBridgeOutdated() {
ch := make(chan string)
b.events.Add(events.UpgradeApplicationEvent, ch)
for range ch {
isApplicationOutdated = true
b.closeAllConnections()
}
}
func (b *Bridge) closeAllConnections() {
for _, user := range b.users {
user.closeAllConnections()
}
}
// Login authenticates a user.
// The login flow:
// * Authenticate user:
// client, auth, err := bridge.Authenticate(username, password)
//
// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
// auth2FA, err := client.Auth2FA(twoFactorCode)
//
// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
// and then finish the login procedure.
// user, err := bridge.FinishLogin(client, auth, mailboxPassword)
func (b *Bridge) Login(username, password string) (loginClient PMAPIProvider, auth *pmapi.Auth, err error) {
log.WithField("username", username).Trace("Logging in to bridge")
b.crashBandicoot(username)
// We need to use "login" client because we need userID to properly
// assign access tokens into token manager.
loginClient = b.pmapiClientFactory("login")
authInfo, err := loginClient.AuthInfo(username)
if err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
return nil, nil, err
}
if auth, err = loginClient.Auth(username, password, authInfo); err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth for user")
return loginClient, auth, err
}
return loginClient, auth, nil
}
// FinishLogin finishes the login procedure and adds the user into the credentials store.
// See `Login` for more details of the login flow.
func (b *Bridge) FinishLogin(loginClient PMAPIProvider, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
log.Trace("Finishing bridge login")
defer func() {
if err == pmapi.ErrUpgradeApplication {
b.events.Emit(events.UpgradeApplicationEvent, "")
}
}()
b.lock.Lock()
defer b.lock.Unlock()
mbPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
if err != nil {
log.WithError(err).Error("Could not hash mailbox password")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after hash password failed.")
}
return
}
if _, err = loginClient.Unlock(mbPassword); err != nil {
log.WithError(err).Error("Could not decrypt keyring")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after unlock failed.")
}
return
}
apiUser, err := loginClient.CurrentUser()
if err != nil {
log.WithError(err).Error("Could not get login API user")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
}
return
}
user, hasUser := b.hasUser(apiUser.ID)
// If the user exists and is logged in, we don't want to do anything.
if hasUser && user.IsConnected() {
err = errors.New("user is already logged in")
log.WithError(err).Warn("User is already logged in")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Warn("Could not discard auth generated during second login")
}
return
}
apiToken := auth.UID() + ":" + auth.RefreshToken
apiClient := b.pmapiClientFactory(apiUser.ID)
auth, err = apiClient.AuthRefresh(apiToken)
if err != nil {
log.WithError(err).Error("Could refresh token in new client")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Warn("Could not discard auth generated after auth refresh")
}
return
}
// We load the current user again because it should now have addresses loaded.
apiUser, err = apiClient.CurrentUser()
if err != nil {
log.WithError(err).Error("Could not get current API user")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
}
return
}
apiToken = auth.UID() + ":" + auth.RefreshToken
activeEmails := apiClient.Addresses().ActiveEmails()
if _, err = b.credStorer.Add(apiUser.ID, apiUser.Name, apiToken, mbPassword, activeEmails); err != nil {
log.WithError(err).Error("Could not add user to credentials store")
return
}
// If it's a new user, generate the user object.
if !hasUser {
user, err = newUser(b.panicHandler, apiUser.ID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
if err != nil {
log.WithField("user", apiUser.ID).WithError(err).Error("Could not create user")
return
}
}
// Set up the user auth and store (which we do for both new and existing users).
if err = user.init(b.idleUpdates, apiClient); err != nil {
log.WithField("user", user.userID).WithError(err).Error("Could not initialise user")
return
}
if !hasUser {
b.users = append(b.users, user)
b.SendMetric(m.New(m.Setup, m.NewUser, m.NoLabel))
}
b.events.Emit(events.UserRefreshEvent, apiUser.ID)
return user, err
}
// GetUsers returns all added users into keychain (even logged out users).
func (b *Bridge) GetUsers() []*User {
b.lock.RLock()
defer b.lock.RUnlock()
return b.users
}
// GetUser returns a user by `query` which is compared to users' ID, username
// or any attached e-mail address.
func (b *Bridge) GetUser(query string) (*User, error) {
b.crashBandicoot(query)
b.lock.RLock()
defer b.lock.RUnlock()
for _, user := range b.users {
if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
return user, nil
}
for _, address := range user.GetAddresses() {
if strings.EqualFold(address, query) {
return user, nil
}
}
}
return nil, errors.New("user " + query + " not found")
}
// ClearData closes all connections (to release db files and so on) and clears all data.
func (b *Bridge) ClearData() error {
var result *multierror.Error
for _, user := range b.users {
if err := user.Logout(); err != nil {
result = multierror.Append(result, err)
}
if err := user.closeStore(); err != nil {
result = multierror.Append(result, err)
}
}
if err := b.config.ClearData(); err != nil {
result = multierror.Append(result, err)
}
return result.ErrorOrNil()
}
// DeleteUser deletes user completely; it logs user out from the API, stops any
// active connection, deletes from credentials store and removes from the Bridge struct.
func (b *Bridge) DeleteUser(userID string, clearStore bool) error {
b.lock.Lock()
defer b.lock.Unlock()
log := log.WithField("user", userID)
for idx, user := range b.users {
if user.ID() == userID {
if err := user.Logout(); err != nil {
log.WithError(err).Error("Cannot logout user")
// We can try to continue to remove the user.
// Token will still be valid, but will expire eventually.
}
if err := user.closeStore(); err != nil {
log.WithError(err).Error("Failed to close user store")
}
if clearStore {
// Clear cache after closing connections (done in logout).
if err := user.clearStore(); err != nil {
log.WithError(err).Error("Failed to clear user")
}
}
if err := b.credStorer.Delete(userID); err != nil {
log.WithError(err).Error("Cannot remove user")
return err
}
b.users = append(b.users[:idx], b.users[idx+1:]...)
return nil
}
}
return errors.New("user " + userID + " not found")
} }
// ReportBug reports a new bug from the user. // ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error { func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
c := b.clientManager.GetAnonymousClient() apiClient := b.pmapiClientFactory("bug_reporter")
defer c.Logout()
title := "[Bridge] Bug" title := "[Bridge] Bug"
report := pmapi.ReportReq{ err := apiClient.ReportBugWithEmailClient(
OS: osType, osType,
OSVersion: osVersion, osVersion,
Browser: emailClient, title,
Title: title, description,
Description: description, accountName,
Username: accountName, address,
Email: address, emailClient,
} )
if err != nil {
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err) log.Error("Reporting bug failed: ", err)
return err return err
} }
log.Info("Bug successfully reported") log.Info("Bug successfully reported")
return nil return nil
} }
// GetUpdateChannel returns currently set update channel. // SendMetric sends a metric. We don't want to return any errors, only log them.
func (b *Bridge) GetUpdateChannel() updater.UpdateChannel { func (b *Bridge) SendMetric(m m.Metric) {
return updater.UpdateChannel(b.settings.Get(settings.UpdateChannelKey)) apiClient := b.pmapiClientFactory("metric_reporter")
} cat, act, lab := m.Get()
err := apiClient.SendSimpleMetric(string(cat), string(act), string(lab))
// 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) (needRestart bool, err error) {
b.settings.Set(settings.UpdateChannelKey, string(channel))
version, err := b.updater.Check()
if err != nil { if err != nil {
return false, err log.Error("Sending metric failed: ", err)
}
log.WithFields(logrus.Fields{
"cat": cat,
"act": act,
"lab": lab,
}).Debug("Metric successfully sent")
} }
// We have to deal right away only with downgrade - that action needs to // GetCurrentClient returns currently connected client (e.g. Thunderbird).
// clear data and updates, and install bridge right away. But regular func (b *Bridge) GetCurrentClient() string {
// upgrade can be leaved out for periodic check. res := b.userAgentClientName
if !b.updater.IsDowngrade(version) { if b.userAgentClientVersion != "" {
return false, nil res = res + " " + b.userAgentClientVersion
}
return res
} }
if err := b.Users.ClearData(); err != nil { // SetCurrentClient updates client info (e.g. Thunderbird) and sets the user agent
log.WithError(err).Error("Failed to clear data while downgrading channel") // on pmapi. By default no client is used, IMAP has to detect it on first login.
} func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
if err := b.locations.ClearUpdates(); err != nil { b.userAgentClientName = clientName
log.WithError(err).Error("Failed to clear updates while downgrading channel") b.userAgentClientVersion = clientVersion
b.updateCurrentUserAgent()
} }
if err := b.updater.InstallUpdate(version); err != nil { // SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
return false, err // `runtime.GOOS`, but this can be overridden in case of better detection.
func (b *Bridge) SetCurrentOS(os string) {
b.userAgentOS = os
b.updateCurrentUserAgent()
} }
return true, b.versioner.RemoveOtherVersions(version.Version) // GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
func (b *Bridge) GetIMAPUpdatesChannel() chan interface{} {
if b.idleUpdates == nil {
log.Warn("Bridge updates channel is nil")
} }
// GetKeychainApp returns current keychain helper. return b.idleUpdates
func (b *Bridge) GetKeychainApp() string {
return b.settings.Get(settings.PreferredKeychainKey)
} }
// SetKeychainApp sets current keychain helper. // AllowDoH instructs bridge to use DoH to access an API proxy if necessary.
func (b *Bridge) SetKeychainApp(helper string) { // It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
b.settings.Set(settings.PreferredKeychainKey, helper) func AllowDoH() {
pmapi.GlobalAllowDoH()
}
// DisallowDoH instructs bridge to not use DoH to access an API proxy if necessary.
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func DisallowDoH() {
pmapi.GlobalDisallowDoH()
}
func (b *Bridge) updateCurrentUserAgent() {
UpdateCurrentUserAgent(b.version, b.userAgentOS, b.userAgentClientName, b.userAgentClientVersion)
}
// hasUser returns whether the bridge currently has a user with ID `id`.
func (b *Bridge) hasUser(id string) (user *User, ok bool) {
for _, u := range b.users {
if u.ID() == id {
user, ok = u, true
return
}
}
return
}
// "Easter egg" for testing purposes.
func (b *Bridge) crashBandicoot(username string) {
if username == "crash@bandicoot" {
panic("Your wish is my command… I crash!")
}
} }

View File

@ -0,0 +1,233 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestBridgeFinishLoginBadPassword(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Init bridge with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Set up mocks for FinishLogin.
err := errors.New("bad password")
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, err)
m.pmapiClient.EXPECT().Logout().Return(nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
}
func TestBridgeFinishLoginUpgradeApplication(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Init bridge with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Set up mocks for FinishLogin.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, pmapi.ErrUpgradeApplication)
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
err := errors.New("Cannot logout when upgrade needed")
m.pmapiClient.EXPECT().Logout().Return(err)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
}
func refreshWithToken(token string) *pmapi.Auth {
return &pmapi.Auth{
RefreshToken: token,
KeySalt: "", // No salting in tests.
}
}
func credentialsWithToken(token string) *credentials.Credentials {
tmp := &credentials.Credentials{}
*tmp = *testCredentials
tmp.APIToken = token
return tmp
}
func TestBridgeFinishLoginNewUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Bridge finds no users in the keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Get user to be able to setup new client with proper userID.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
// Setup of new client.
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
// Set up mocks for authorising the new user (in user.init).
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil).Times(2)
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
// Set up mocks for creating the user's store (in store.New).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
// Emit event for new user and send metrics.
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.NewUser), string(metrics.NoLabel))
// Set up mocks for starting the store's event loop (in store.New).
// The event loop runs in another goroutine so this might happen at any time.
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Set up mocks for performing the initial store sync.
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
}
func TestBridgeFinishLoginExistingUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
loggedOutCreds := *testCredentials
loggedOutCreds.APIToken = ""
loggedOutCreds.MailboxPassword = ""
// Bridge finds one logged out user in the keychain.
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
// New user
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
// Init user
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrInvalidToken)
m.pmapiClient.EXPECT().Addresses().Return(nil)
// Get user to be able to setup new client with proper userID.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
// Setup of new client.
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
// Set up mocks for authorising the new user (in user.init).
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil)
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
// Set up mocks for creating the user's store (in store.New).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
// Reload account list in GUI.
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
// Set up mocks for starting the store's event loop (in store.New)
// The event loop runs in another goroutine so this might happen at any time.
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Set up mocks for performing the initial store sync.
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
}
func TestBridgeDoubleLogin(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Firstly, start bridge with existing user...
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
// Then, try to log in again...
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Logout()
_, err := bridge.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
assert.Equal(t, "user is already logged in", err.Error())
}
func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) {
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
user, err := bridge.FinishLogin(m.pmapiClient, auth, mailboxPassword)
waitForEvents()
assert.Equal(t, expectedErr, err)
if expectedUserID != "" {
assert.Equal(t, expectedUserID, user.ID())
assert.Equal(t, 1, len(bridge.users))
assert.Equal(t, expectedUserID, bridge.users[0].ID())
} else {
assert.Equal(t, (*User)(nil), user)
assert.Equal(t, 0, len(bridge.users))
}
}

View File

@ -0,0 +1,162 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"errors"
"testing"
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestNewBridgeNoKeychain(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain"))
checkBridgeNew(t, m, []*credentials.Credentials{})
}
func TestNewBridgeWithoutUsersInCredentialsStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
checkBridgeNew(t, m, []*credentials.Credentials{})
}
func TestNewBridgeWithDisconnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
m.pmapiClient.EXPECT().Addresses().Return(nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
}
func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
}
func TestNewBridgeWithConnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
// Set up mocks for store initialisation for the authorized user.
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentials})
}
// Tests two users with different states and checks also the order from
// credentials store is kept also in array of Bridge users.
func TestNewBridgeWithUsers(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().List().Return([]string{"user", "user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
// Set up mocks for store initialisation for the unauth user.
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
m.pmapiClient.EXPECT().Addresses().Return(nil)
// Set up mocks for store initialisation for the authorized user.
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
}
func TestNewBridgeFirstStart(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(true)
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.FirstStart), gomock.Any())
testNewBridge(t, m)
}
func checkBridgeNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
assert.Equal(m.t, len(expectedCredentials), len(bridge.GetUsers()))
credentials := []*credentials.Credentials{}
for _, user := range bridge.users {
credentials = append(credentials, user.creds)
}
assert.Equal(m.t, expectedCredentials, credentials)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -15,32 +15,27 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package users package bridge
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"runtime/debug"
"testing" "testing"
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
bridgemocks "github.com/ProtonMail/proton-bridge/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/sentry" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
if os.Getenv("VERBOSITY") == "fatal" {
logrus.SetLevel(logrus.FatalLevel)
}
if os.Getenv("VERBOSITY") == "trace" { if os.Getenv("VERBOSITY") == "trace" {
logrus.SetLevel(logrus.TraceLevel) logrus.SetLevel(logrus.TraceLevel)
} }
@ -50,9 +45,11 @@ func TestMain(m *testing.M) {
var ( var (
testAuth = &pmapi.Auth{ //nolint[gochecknoglobals] testAuth = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "tok", RefreshToken: "tok",
KeySalt: "", // No salting in tests.
} }
testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals] testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "reftok", RefreshToken: "reftok",
KeySalt: "", // No salting in tests.
} }
testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals] testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals]
@ -128,39 +125,18 @@ type mocks struct {
t *testing.T t *testing.T
ctrl *gomock.Controller ctrl *gomock.Controller
locator *usersmocks.MockLocator config *bridgemocks.MockConfiger
PanicHandler *usersmocks.MockPanicHandler PanicHandler *bridgemocks.MockPanicHandler
clientManager *usersmocks.MockClientManager prefProvider *bridgemocks.MockPreferenceProvider
credentialsStore *usersmocks.MockCredentialsStorer pmapiClient *bridgemocks.MockPMAPIProvider
storeMaker *usersmocks.MockStoreMaker credentialsStore *bridgemocks.MockCredentialsStorer
eventListener *MockListener eventListener *MockListener
pmapiClient *pmapimocks.MockClient
storeCache *store.Cache storeCache *store.Cache
} }
type fullStackReporter struct {
T testing.TB
}
func (fr *fullStackReporter) Errorf(format string, args ...interface{}) {
fmt.Printf("err: "+format+"\n", args...)
fr.T.Fail()
}
func (fr *fullStackReporter) Fatalf(format string, args ...interface{}) {
debug.PrintStack()
fmt.Printf("fail: "+format+"\n", args...)
fr.T.FailNow()
}
func initMocks(t *testing.T) mocks { func initMocks(t *testing.T) mocks {
var mockCtrl *gomock.Controller mockCtrl := gomock.NewController(t)
if os.Getenv("VERBOSITY") == "trace" {
mockCtrl = gomock.NewController(&fullStackReporter{t})
} else {
mockCtrl = gomock.NewController(t)
}
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db") cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache") require.NoError(t, err, "could not get temporary file for store cache")
@ -169,84 +145,84 @@ func initMocks(t *testing.T) mocks {
t: t, t: t,
ctrl: mockCtrl, ctrl: mockCtrl,
locator: usersmocks.NewMockLocator(mockCtrl), config: bridgemocks.NewMockConfiger(mockCtrl),
PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl), PanicHandler: bridgemocks.NewMockPanicHandler(mockCtrl),
clientManager: usersmocks.NewMockClientManager(mockCtrl), pmapiClient: bridgemocks.NewMockPMAPIProvider(mockCtrl),
credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl), prefProvider: bridgemocks.NewMockPreferenceProvider(mockCtrl),
storeMaker: usersmocks.NewMockStoreMaker(mockCtrl), credentialsStore: bridgemocks.NewMockCredentialsStorer(mockCtrl),
eventListener: NewMockListener(mockCtrl), eventListener: NewMockListener(mockCtrl),
pmapiClient: pmapimocks.NewMockClient(mockCtrl),
storeCache: store.NewCache(cacheFile.Name()), storeCache: store.NewCache(cacheFile.Name()),
} }
// Ignore heartbeat calls because they always happen.
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Heartbeat), gomock.Any(), gomock.Any()).AnyTimes()
m.prefProvider.EXPECT().Get(preferences.NextHeartbeatKey).AnyTimes()
m.prefProvider.EXPECT().Set(preferences.NextHeartbeatKey, gomock.Any()).AnyTimes()
// Called during clean-up. // Called during clean-up.
m.PanicHandler.EXPECT().HandlePanic().AnyTimes() m.PanicHandler.EXPECT().HandlePanic().AnyTimes()
// Set up store factory.
m.storeMaker.EXPECT().New(gomock.Any()).DoAndReturn(func(user store.BridgeUser) (*store.Store, error) {
var sentryReporter *sentry.Reporter // Sentry reporter is not used under unit tests.
dbFile, err := ioutil.TempFile("", "bridge-store-db-*.db")
require.NoError(t, err, "could not get temporary file for store db")
return store.New(sentryReporter, m.PanicHandler, user, m.clientManager, m.eventListener, dbFile.Name(), m.storeCache)
}).AnyTimes()
m.storeMaker.EXPECT().Remove(gomock.Any()).AnyTimes()
return m return m
} }
func testNewUsersWithUsers(t *testing.T, m mocks) *Users { func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge {
// Events are asynchronous
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).Times(2)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).Times(2)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).Times(2)
gomock.InOrder(
m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil),
// Init for user. // Init for user.
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Init for users. // Init for users.
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil), m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses), m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil).Times(2)
) m.credentialsStore.EXPECT().UpdateToken("users", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
users := testNewUsers(t, m) m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil)
user, _ := users.GetUser("user") return testNewBridge(t, m)
mockAuthUpdate(user, "reftok", m)
user, _ = users.GetUser("user")
mockAuthUpdate(user, "reftok", m)
return users
} }
func testNewUsers(t *testing.T, m mocks) *Users { //nolint[unparam] func testNewBridge(t *testing.T, m mocks) *Bridge {
m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any()) cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth)) require.NoError(t, err, "could not get temporary file for store cache")
users := New(m.locator, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore, m.storeMaker, true) m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(false).AnyTimes()
m.prefProvider.EXPECT().GetBool(preferences.AllowProxyKey).Return(false).AnyTimes()
m.config.EXPECT().GetDBDir().Return("/tmp").AnyTimes()
m.config.EXPECT().GetIMAPCachePath().Return(cacheFile.Name()).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any()).AnyTimes()
m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any())
pmapiClientFactory := func(userID string) PMAPIProvider {
log.WithField("userID", userID).Info("Creating new pmclient")
return m.pmapiClient
}
bridge := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, "ver", pmapiClientFactory, m.credentialsStore)
waitForEvents() waitForEvents()
return users return bridge
} }
func cleanUpUsersData(b *Users) { func cleanUpBridgeUserData(b *Bridge) {
for _, user := range b.users { for _, user := range b.users {
_ = user.clearStore() _ = user.clearStore()
} }
@ -256,11 +232,8 @@ func TestClearData(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) bridge := testNewBridgeWithUsers(t, m)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) defer cleanUpBridgeUserData(bridge)
users := testNewUsersWithUsers(t, m)
defer cleanUpUsersData(users)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me")
@ -275,47 +248,9 @@ func TestClearData(t *testing.T) {
m.credentialsStore.EXPECT().Logout("users").Return(nil) m.credentialsStore.EXPECT().Logout("users").Return(nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil) m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil)
m.locator.EXPECT().Clear() m.config.EXPECT().ClearData().Return(nil)
require.NoError(t, users.ClearData()) require.NoError(t, bridge.ClearData())
waitForEvents()
}
func mockEventLoopNoAction(m mocks) {
// Set up mocks for starting the store's event loop (in store.New).
// The event loop runs in another goroutine so this might happen at any time.
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
}
func mockConnectedUser(m mocks) {
gomock.InOrder(
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
// Set up mocks for store initialisation for the authorized user.
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
)
}
// mockAuthUpdate simulates users calling UpdateAuthToken on the given user.
// This would normally be done by users when it receives an auth from the ClientManager,
// but as we don't have a full users instance here, we do this manually.
func mockAuthUpdate(user *User, token string, m mocks) {
gomock.InOrder(
m.credentialsStore.EXPECT().UpdateToken("user", ":"+token).Return(nil),
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(token), nil),
)
user.updateAuthToken(refreshWithToken(token))
waitForEvents() waitForEvents()
} }

View File

@ -0,0 +1,121 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/stretchr/testify/assert"
)
func TestGetNoUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "nouser", -1, "user nouser not found")
}
func TestGetUserByID(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "user", 0, "")
checkBridgeGetUser(t, m, "users", 1, "")
}
func TestGetUserByName(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "username", 0, "")
checkBridgeGetUser(t, m, "usersname", 1, "")
}
func TestGetUserByEmail(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "user@pm.me", 0, "")
checkBridgeGetUser(t, m, "users@pm.me", 1, "")
checkBridgeGetUser(t, m, "anotheruser@pm.me", 1, "")
checkBridgeGetUser(t, m, "alsouser@pm.me", 1, "")
}
func TestDeleteUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Delete("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := bridge.DeleteUser("user", true)
assert.NoError(t, err)
assert.Equal(t, 1, len(bridge.users))
}
// Even when logout fails, delete is done.
func TestDeleteUserWithFailingLogout(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed"))
m.credentialsStore.EXPECT().Delete("user").Return(nil).Times(2)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := bridge.DeleteUser("user", true)
assert.NoError(t, err)
assert.Equal(t, 1, len(bridge.users))
}
func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
user, err := bridge.GetUser(query)
waitForEvents()
if expectedError != "" {
assert.Equal(m.t, expectedError, err.Error())
} else {
assert.NoError(m.t, err)
}
var expectedUser *User
if index >= 0 {
expectedUser = bridge.users[index]
}
assert.Equal(m.t, expectedUser, user)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -27,20 +27,16 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const ( const sep = "\x00"
sep = "\x00"
itemLengthBridge = 9
itemLengthImportExport = 6 // Old format for Import-Export.
)
var ( var (
log = logrus.WithField("pkg", "credentials") //nolint[gochecknoglobals] log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
ErrWrongFormat = errors.New("malformed credentials") ErrWrongFormat = errors.New("backend/creds: malformed password")
) )
type Credentials struct { type Credentials struct {
@ -90,7 +86,7 @@ func (s *Credentials) Unmarshal(secret string) error {
} }
items := strings.Split(string(b), sep) items := strings.Split(string(b), sep)
if len(items) != itemLengthBridge && len(items) != itemLengthImportExport { if len(items) != 9 {
return ErrWrongFormat return ErrWrongFormat
} }
@ -98,9 +94,6 @@ func (s *Credentials) Unmarshal(secret string) error {
s.Emails = items[1] s.Emails = items[1]
s.APIToken = items[2] s.APIToken = items[2]
s.MailboxPassword = items[3] s.MailboxPassword = items[3]
switch len(items) {
case itemLengthBridge:
s.BridgePassword = items[4] s.BridgePassword = items[4]
s.Version = items[5] s.Version = items[5]
if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil { if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil {
@ -112,13 +105,6 @@ func (s *Credentials) Unmarshal(secret string) error {
if s.IsCombinedAddressMode = false; items[8] == "1" { if s.IsCombinedAddressMode = false; items[8] == "1" {
s.IsCombinedAddressMode = true s.IsCombinedAddressMode = true
} }
case itemLengthImportExport:
s.Version = items[4]
if _, err = fmt.Sscan(items[5], &s.Timestamp); err != nil {
s.Timestamp = 0
}
}
return nil return nil
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -15,8 +15,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !imaptest
package credentials package credentials
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -18,6 +18,7 @@
package credentials package credentials
import ( import (
"errors"
"fmt" "fmt"
"sort" "sort"
"sync" "sync"
@ -31,12 +32,15 @@ var storeLocker = sync.RWMutex{} //nolint[gochecknoglobals]
// Store is an encrypted credentials store. // Store is an encrypted credentials store.
type Store struct { type Store struct {
secrets *keychain.Keychain secrets *keychain.Access
} }
// NewStore creates a new encrypted credentials store. // NewStore creates a new encrypted credentials store.
func NewStore(keychain *keychain.Keychain) *Store { func NewStore() (*Store, error) {
return &Store{secrets: keychain} secrets, err := keychain.NewAccess("bridge")
return &Store{
secrets: secrets,
}, err
} }
func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails []string) (creds *Credentials, err error) { func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails []string) (creds *Credentials, err error) {
@ -49,6 +53,10 @@ func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails [
"emails": emails, "emails": emails,
}).Trace("Adding new credentials") }).Trace("Adding new credentials")
if err = s.checkKeychain(); err != nil {
return
}
creds = &Credentials{ creds = &Credentials{
UserID: userID, UserID: userID,
Name: userName, Name: userName,
@ -59,9 +67,18 @@ func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails [
creds.SetEmailList(emails) creds.SetEmailList(emails)
currentCredentials, err := s.get(userID) var has bool
if err == nil { if has, err = s.has(userID); err != nil {
log.WithField("userID", userID).WithError(err).Error("Could not check if user credentials already exist")
return
}
if has {
log.Info("Updating credentials of existing user") log.Info("Updating credentials of existing user")
currentCredentials, err := s.get(userID)
if err != nil {
return nil, err
}
creds.BridgePassword = currentCredentials.BridgePassword creds.BridgePassword = currentCredentials.BridgePassword
creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode
creds.Timestamp = currentCredentials.Timestamp creds.Timestamp = currentCredentials.Timestamp
@ -108,20 +125,6 @@ func (s *Store) UpdateEmails(userID string, emails []string) error {
return s.saveCredentials(credentials) return s.saveCredentials(credentials)
} }
func (s *Store) UpdatePassword(userID, password string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.MailboxPassword = password
return s.saveCredentials(credentials)
}
func (s *Store) UpdateToken(userID, apiToken string) error { func (s *Store) UpdateToken(userID, apiToken string) error {
storeLocker.Lock() storeLocker.Lock()
defer storeLocker.Unlock() defer storeLocker.Unlock()
@ -157,6 +160,10 @@ func (s *Store) List() (userIDs []string, err error) {
log.Trace("Listing credentials in credentials store") log.Trace("Listing credentials in credentials store")
if err = s.checkKeychain(); err != nil {
return
}
var allUserIDs []string var allUserIDs []string
if allUserIDs, err = s.secrets.List(); err != nil { if allUserIDs, err = s.secrets.List(); err != nil {
log.WithError(err).Error("Could not list credentials") log.WithError(err).Error("Could not list credentials")
@ -175,13 +182,6 @@ func (s *Store) List() (userIDs []string, err error) {
continue continue
} }
// Old credentials using username as a key does not work with new code.
// We need to ask user to login again to get ID from API and migrate creds.
if creds.UserID == creds.Name && creds.APIToken != "" {
creds.Logout()
_ = s.saveCredentials(creds)
}
credentialList = append(credentialList, creds) credentialList = append(credentialList, creds)
} }
@ -225,13 +225,48 @@ func (s *Store) Get(userID string) (creds *Credentials, err error) {
storeLocker.RLock() storeLocker.RLock()
defer storeLocker.RUnlock() defer storeLocker.RUnlock()
var has bool
if has, err = s.has(userID); err != nil {
log.WithError(err).Error("Could not check for credentials")
return
}
if !has {
err = errors.New("no credentials found for given userID")
return
}
return s.get(userID) return s.get(userID)
} }
func (s *Store) has(userID string) (has bool, err error) {
if err = s.checkKeychain(); err != nil {
return
}
var ids []string
if ids, err = s.secrets.List(); err != nil {
log.WithError(err).Error("Could not list credentials")
return
}
for _, id := range ids {
if id == userID {
has = true
}
}
return
}
func (s *Store) get(userID string) (creds *Credentials, err error) { func (s *Store) get(userID string) (creds *Credentials, err error) {
log := log.WithField("user", userID) log := log.WithField("user", userID)
_, secret, err := s.secrets.Get(userID) if err = s.checkKeychain(); err != nil {
return
}
secret, err := s.secrets.Get(userID)
if err != nil { if err != nil {
log.WithError(err).Error("Could not get credentials from native keychain") log.WithError(err).Error("Could not get credentials from native keychain")
return return
@ -250,15 +285,32 @@ func (s *Store) get(userID string) (creds *Credentials, err error) {
// saveCredentials encrypts and saves password to the keychain store. // saveCredentials encrypts and saves password to the keychain store.
func (s *Store) saveCredentials(credentials *Credentials) (err error) { func (s *Store) saveCredentials(credentials *Credentials) (err error) {
credentials.Version = keychain.Version if err = s.checkKeychain(); err != nil {
return
}
credentials.Version = keychain.KeychainVersion
return s.secrets.Put(credentials.UserID, credentials.Marshal()) return s.secrets.Put(credentials.UserID, credentials.Marshal())
} }
func (s *Store) checkKeychain() (err error) {
if s.secrets == nil {
err = keychain.ErrNoKeychainInstalled
log.WithError(err).Error("Store is unusable")
}
return
}
// Delete removes credentials from the store. // Delete removes credentials from the store.
func (s *Store) Delete(userID string) (err error) { func (s *Store) Delete(userID string) (err error) {
storeLocker.Lock() storeLocker.Lock()
defer storeLocker.Unlock() defer storeLocker.Unlock()
if err = s.checkKeychain(); err != nil {
return
}
return s.secrets.Delete(userID) return s.secrets.Delete(userID)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -67,7 +67,7 @@ func (s *testCredentials) MarshalGob() string {
if err := enc.Encode(s); err != nil { if err := enc.Encode(s); err != nil {
return "" return ""
} }
log.Infof("MarshalGob: %#v\n", buf.String()) fmt.Printf("MarshalGob: %#v\n", buf.String())
return base64.StdEncoding.EncodeToString(buf.Bytes()) return base64.StdEncoding.EncodeToString(buf.Bytes())
} }
@ -88,13 +88,13 @@ func (s *testCredentials) UnmarshalGob(secret string) error {
s.Clear() s.Clear()
b, err := base64.StdEncoding.DecodeString(secret) b, err := base64.StdEncoding.DecodeString(secret)
if err != nil { if err != nil {
log.Infoln("decode base64", b) fmt.Println("decode base64", b)
return err return err
} }
buf := bytes.NewBuffer(b) buf := bytes.NewBuffer(b)
dec := gob.NewDecoder(buf) dec := gob.NewDecoder(buf)
if err = dec.Decode(s); err != nil { if err = dec.Decode(s); err != nil {
log.Info("decode gob", b, buf.Bytes()) fmt.Println("decode gob", b, buf.Bytes())
return err return err
} }
return nil return nil
@ -102,7 +102,7 @@ func (s *testCredentials) UnmarshalGob(secret string) error {
func (s *testCredentials) ToJSON() string { func (s *testCredentials) ToJSON() string {
if b, err := json.Marshal(s); err == nil { if b, err := json.Marshal(s); err == nil {
log.Infof("MarshalJSON: %#v\n", string(b)) fmt.Printf("MarshalJSON: %#v\n", string(b))
return base64.StdEncoding.EncodeToString(b) return base64.StdEncoding.EncodeToString(b)
} }
return "" return ""
@ -134,7 +134,7 @@ func (s *testCredentials) MarshalFmt() string {
s.IsHidden, s.IsHidden,
s.IsCombinedAddressMode, s.IsCombinedAddressMode,
) )
log.Infof("MarshalFmt: %#v\n", buf.String()) fmt.Printf("MarshalFmt: %#v\n", buf.String())
return base64.StdEncoding.EncodeToString(buf.Bytes()) return base64.StdEncoding.EncodeToString(buf.Bytes())
} }
@ -144,7 +144,7 @@ func (s *testCredentials) UnmarshalFmt(secret string) error {
return err return err
} }
buf := bytes.NewBuffer(b) buf := bytes.NewBuffer(b)
log.Infoln("decode fmt", b, buf.Bytes()) fmt.Println("decode fmt", b, buf.Bytes())
_, err = fmt.Fscanf( _, err = fmt.Fscanf(
buf, secretFormat, buf, secretFormat,
&s.UserID, &s.UserID,
@ -190,7 +190,7 @@ func (s *testCredentials) MarshalStrings() string { // this is the most space ef
str := strings.Join(items, sep) str := strings.Join(items, sep)
log.Infof("MarshalJoin: %#v\n", str) fmt.Printf("MarshalJoin: %#v\n", str)
return base64.StdEncoding.EncodeToString([]byte(str)) return base64.StdEncoding.EncodeToString([]byte(str))
} }
@ -237,37 +237,37 @@ func (s *testCredentials) IsSame(rhs *testCredentials) bool {
func TestMarshalFormats(t *testing.T) { func TestMarshalFormats(t *testing.T) {
input := testCredentials{UserID: "007", Emails: "ja@pm.me;jakub@cu.th", Timestamp: 152469263742, IsHidden: true} input := testCredentials{UserID: "007", Emails: "ja@pm.me;jakub@cu.th", Timestamp: 152469263742, IsHidden: true}
log.Infof("input %#v\n", input) fmt.Printf("input %#v\n", input)
secretStrings := input.MarshalStrings() secretStrings := input.MarshalStrings()
log.Infof("secretStrings %#v %d\n", secretStrings, len(secretStrings)) fmt.Printf("secretStrings %#v %d\n", secretStrings, len(secretStrings))
secretGob := input.MarshalGob() secretGob := input.MarshalGob()
log.Infof("secretGob %#v %d\n", secretGob, len(secretGob)) fmt.Printf("secretGob %#v %d\n", secretGob, len(secretGob))
secretJSON := input.ToJSON() secretJSON := input.ToJSON()
log.Infof("secretJSON %#v %d\n", secretJSON, len(secretJSON)) fmt.Printf("secretJSON %#v %d\n", secretJSON, len(secretJSON))
secretFmt := input.MarshalFmt() secretFmt := input.MarshalFmt()
log.Infof("secretFmt %#v %d\n", secretFmt, len(secretFmt)) fmt.Printf("secretFmt %#v %d\n", secretFmt, len(secretFmt))
output := testCredentials{APIToken: "refresh"} output := testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalStrings(secretStrings)) require.NoError(t, output.UnmarshalStrings(secretStrings))
log.Infof("strings out %#v \n", output) fmt.Printf("strings out %#v \n", output)
require.True(t, input.IsSame(&output), "strings out not same") require.True(t, input.IsSame(&output), "strings out not same")
output = testCredentials{APIToken: "refresh"} output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalGob(secretGob)) require.NoError(t, output.UnmarshalGob(secretGob))
log.Infof("gob out %#v\n \n", output) fmt.Printf("gob out %#v\n \n", output)
assert.Equal(t, input, output) assert.Equal(t, input, output)
output = testCredentials{APIToken: "refresh"} output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.FromJSON(secretJSON)) require.NoError(t, output.FromJSON(secretJSON))
log.Infof("json out %#v \n", output) fmt.Printf("json out %#v \n", output)
require.True(t, input.IsSame(&output), "json out not same") require.True(t, input.IsSame(&output), "json out not same")
/* /*
// Simple Fscanf not working! // Simple Fscanf not working!
output = testCredentials{APIToken: "refresh"} output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalFmt(secretFmt)) require.NoError(t, output.UnmarshalFmt(secretFmt))
log.Infof("fmt out %#v \n", output) fmt.Printf("fmt out %#v \n", output)
require.True(t, input.IsSame(&output), "fmt out not same") require.True(t, input.IsSame(&output), "fmt out not same")
*/ */
} }
@ -285,13 +285,13 @@ func TestMarshal(t *testing.T) {
IsHidden: true, IsHidden: true,
IsCombinedAddressMode: false, IsCombinedAddressMode: false,
} }
log.Infof("input %#v\n", input) fmt.Printf("input %#v\n", input)
secret := input.Marshal() secret := input.Marshal()
log.Infof("secret %#v %d\n", secret, len(secret)) fmt.Printf("secret %#v %d\n", secret, len(secret))
output := Credentials{APIToken: "refresh"} output := Credentials{APIToken: "refresh"}
require.NoError(t, output.Unmarshal(secret)) require.NoError(t, output.Unmarshal(secret))
log.Infof("output %#v\n", output) fmt.Printf("output %#v\n", output)
assert.Equal(t, input, output) assert.Equal(t, input, output)
} }

View File

@ -0,0 +1,22 @@
// 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/>.
// Code generated by ./credits.sh at Thu Apr 9 13:39:29 CEST 2020. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-imap-quota;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/danieljoos/wincred;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: ./listener/listener.go // Source: ./listener/listener.go
// Package users is a generated GoMock package. // Package bridge is a generated GoMock package.
package users package bridge
import ( import (
reflect "reflect" reflect "reflect"

View File

@ -0,0 +1,923 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/bridge (interfaces: Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer)
// Package mocks is a generated GoMock package.
package mocks
import (
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
crypto "github.com/ProtonMail/gopenpgp/crypto"
gomock "github.com/golang/mock/gomock"
io "io"
reflect "reflect"
)
// MockConfiger is a mock of Configer interface
type MockConfiger struct {
ctrl *gomock.Controller
recorder *MockConfigerMockRecorder
}
// MockConfigerMockRecorder is the mock recorder for MockConfiger
type MockConfigerMockRecorder struct {
mock *MockConfiger
}
// NewMockConfiger creates a new mock instance
func NewMockConfiger(ctrl *gomock.Controller) *MockConfiger {
mock := &MockConfiger{ctrl: ctrl}
mock.recorder = &MockConfigerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockConfiger) EXPECT() *MockConfigerMockRecorder {
return m.recorder
}
// ClearData mocks base method
func (m *MockConfiger) ClearData() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearData")
ret0, _ := ret[0].(error)
return ret0
}
// ClearData indicates an expected call of ClearData
func (mr *MockConfigerMockRecorder) ClearData() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearData", reflect.TypeOf((*MockConfiger)(nil).ClearData))
}
// GetAPIConfig mocks base method
func (m *MockConfiger) GetAPIConfig() *pmapi.ClientConfig {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAPIConfig")
ret0, _ := ret[0].(*pmapi.ClientConfig)
return ret0
}
// GetAPIConfig indicates an expected call of GetAPIConfig
func (mr *MockConfigerMockRecorder) GetAPIConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIConfig", reflect.TypeOf((*MockConfiger)(nil).GetAPIConfig))
}
// GetDBDir mocks base method
func (m *MockConfiger) GetDBDir() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDBDir")
ret0, _ := ret[0].(string)
return ret0
}
// GetDBDir indicates an expected call of GetDBDir
func (mr *MockConfigerMockRecorder) GetDBDir() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBDir", reflect.TypeOf((*MockConfiger)(nil).GetDBDir))
}
// GetIMAPCachePath mocks base method
func (m *MockConfiger) GetIMAPCachePath() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIMAPCachePath")
ret0, _ := ret[0].(string)
return ret0
}
// GetIMAPCachePath indicates an expected call of GetIMAPCachePath
func (mr *MockConfigerMockRecorder) GetIMAPCachePath() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIMAPCachePath", reflect.TypeOf((*MockConfiger)(nil).GetIMAPCachePath))
}
// MockPreferenceProvider is a mock of PreferenceProvider interface
type MockPreferenceProvider struct {
ctrl *gomock.Controller
recorder *MockPreferenceProviderMockRecorder
}
// MockPreferenceProviderMockRecorder is the mock recorder for MockPreferenceProvider
type MockPreferenceProviderMockRecorder struct {
mock *MockPreferenceProvider
}
// NewMockPreferenceProvider creates a new mock instance
func NewMockPreferenceProvider(ctrl *gomock.Controller) *MockPreferenceProvider {
mock := &MockPreferenceProvider{ctrl: ctrl}
mock.recorder = &MockPreferenceProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPreferenceProvider) EXPECT() *MockPreferenceProviderMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockPreferenceProvider) Get(arg0 string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// Get indicates an expected call of Get
func (mr *MockPreferenceProviderMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPreferenceProvider)(nil).Get), arg0)
}
// GetBool mocks base method
func (m *MockPreferenceProvider) GetBool(arg0 string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBool", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// GetBool indicates an expected call of GetBool
func (mr *MockPreferenceProviderMockRecorder) GetBool(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockPreferenceProvider)(nil).GetBool), arg0)
}
// GetInt mocks base method
func (m *MockPreferenceProvider) GetInt(arg0 string) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt", arg0)
ret0, _ := ret[0].(int)
return ret0
}
// GetInt indicates an expected call of GetInt
func (mr *MockPreferenceProviderMockRecorder) GetInt(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockPreferenceProvider)(nil).GetInt), arg0)
}
// Set mocks base method
func (m *MockPreferenceProvider) Set(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", arg0, arg1)
}
// Set indicates an expected call of Set
func (mr *MockPreferenceProviderMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockPreferenceProvider)(nil).Set), arg0, arg1)
}
// MockPanicHandler is a mock of PanicHandler interface
type MockPanicHandler struct {
ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder
}
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler
type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler
}
// NewMockPanicHandler creates a new mock instance
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder
}
// HandlePanic mocks base method
func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic")
}
// HandlePanic indicates an expected call of HandlePanic
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
}
// MockPMAPIProvider is a mock of PMAPIProvider interface
type MockPMAPIProvider struct {
ctrl *gomock.Controller
recorder *MockPMAPIProviderMockRecorder
}
// MockPMAPIProviderMockRecorder is the mock recorder for MockPMAPIProvider
type MockPMAPIProviderMockRecorder struct {
mock *MockPMAPIProvider
}
// NewMockPMAPIProvider creates a new mock instance
func NewMockPMAPIProvider(ctrl *gomock.Controller) *MockPMAPIProvider {
mock := &MockPMAPIProvider{ctrl: ctrl}
mock.recorder = &MockPMAPIProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPMAPIProvider) EXPECT() *MockPMAPIProviderMockRecorder {
return m.recorder
}
// Addresses mocks base method
func (m *MockPMAPIProvider) Addresses() pmapi.AddressList {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Addresses")
ret0, _ := ret[0].(pmapi.AddressList)
return ret0
}
// Addresses indicates an expected call of Addresses
func (mr *MockPMAPIProviderMockRecorder) Addresses() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addresses", reflect.TypeOf((*MockPMAPIProvider)(nil).Addresses))
}
// Auth mocks base method
func (m *MockPMAPIProvider) Auth(arg0, arg1 string, arg2 *pmapi.AuthInfo) (*pmapi.Auth, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Auth", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Auth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Auth indicates an expected call of Auth
func (mr *MockPMAPIProviderMockRecorder) Auth(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth", reflect.TypeOf((*MockPMAPIProvider)(nil).Auth), arg0, arg1, arg2)
}
// Auth2FA mocks base method
func (m *MockPMAPIProvider) Auth2FA(arg0 string, arg1 *pmapi.Auth) (*pmapi.Auth2FA, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Auth2FA", arg0, arg1)
ret0, _ := ret[0].(*pmapi.Auth2FA)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Auth2FA indicates an expected call of Auth2FA
func (mr *MockPMAPIProviderMockRecorder) Auth2FA(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth2FA", reflect.TypeOf((*MockPMAPIProvider)(nil).Auth2FA), arg0, arg1)
}
// AuthInfo mocks base method
func (m *MockPMAPIProvider) AuthInfo(arg0 string) (*pmapi.AuthInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthInfo", arg0)
ret0, _ := ret[0].(*pmapi.AuthInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthInfo indicates an expected call of AuthInfo
func (mr *MockPMAPIProviderMockRecorder) AuthInfo(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthInfo", reflect.TypeOf((*MockPMAPIProvider)(nil).AuthInfo), arg0)
}
// AuthRefresh mocks base method
func (m *MockPMAPIProvider) AuthRefresh(arg0 string) (*pmapi.Auth, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthRefresh", arg0)
ret0, _ := ret[0].(*pmapi.Auth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthRefresh indicates an expected call of AuthRefresh
func (mr *MockPMAPIProviderMockRecorder) AuthRefresh(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthRefresh", reflect.TypeOf((*MockPMAPIProvider)(nil).AuthRefresh), arg0)
}
// CountMessages mocks base method
func (m *MockPMAPIProvider) CountMessages(arg0 string) ([]*pmapi.MessagesCount, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountMessages", arg0)
ret0, _ := ret[0].([]*pmapi.MessagesCount)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountMessages indicates an expected call of CountMessages
func (mr *MockPMAPIProviderMockRecorder) CountMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).CountMessages), arg0)
}
// CreateAttachment mocks base method
func (m *MockPMAPIProvider) CreateAttachment(arg0 *pmapi.Attachment, arg1, arg2 io.Reader) (*pmapi.Attachment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateAttachment", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Attachment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateAttachment indicates an expected call of CreateAttachment
func (mr *MockPMAPIProviderMockRecorder) CreateAttachment(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAttachment", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateAttachment), arg0, arg1, arg2)
}
// CreateDraft mocks base method
func (m *MockPMAPIProvider) CreateDraft(arg0 *pmapi.Message, arg1 string, arg2 int) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDraft", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDraft indicates an expected call of CreateDraft
func (mr *MockPMAPIProviderMockRecorder) CreateDraft(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDraft", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateDraft), arg0, arg1, arg2)
}
// CreateLabel mocks base method
func (m *MockPMAPIProvider) CreateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateLabel", arg0)
ret0, _ := ret[0].(*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateLabel indicates an expected call of CreateLabel
func (mr *MockPMAPIProviderMockRecorder) CreateLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateLabel), arg0)
}
// CurrentUser mocks base method
func (m *MockPMAPIProvider) CurrentUser() (*pmapi.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CurrentUser")
ret0, _ := ret[0].(*pmapi.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CurrentUser indicates an expected call of CurrentUser
func (mr *MockPMAPIProviderMockRecorder) CurrentUser() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentUser", reflect.TypeOf((*MockPMAPIProvider)(nil).CurrentUser))
}
// DecryptAndVerifyCards mocks base method
func (m *MockPMAPIProvider) DecryptAndVerifyCards(arg0 []pmapi.Card) ([]pmapi.Card, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DecryptAndVerifyCards", arg0)
ret0, _ := ret[0].([]pmapi.Card)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DecryptAndVerifyCards indicates an expected call of DecryptAndVerifyCards
func (mr *MockPMAPIProviderMockRecorder) DecryptAndVerifyCards(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptAndVerifyCards", reflect.TypeOf((*MockPMAPIProvider)(nil).DecryptAndVerifyCards), arg0)
}
// DeleteLabel mocks base method
func (m *MockPMAPIProvider) DeleteLabel(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteLabel", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteLabel indicates an expected call of DeleteLabel
func (mr *MockPMAPIProviderMockRecorder) DeleteLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).DeleteLabel), arg0)
}
// DeleteMessages mocks base method
func (m *MockPMAPIProvider) DeleteMessages(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteMessages", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteMessages indicates an expected call of DeleteMessages
func (mr *MockPMAPIProviderMockRecorder) DeleteMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).DeleteMessages), arg0)
}
// EmptyFolder mocks base method
func (m *MockPMAPIProvider) EmptyFolder(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EmptyFolder", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// EmptyFolder indicates an expected call of EmptyFolder
func (mr *MockPMAPIProviderMockRecorder) EmptyFolder(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EmptyFolder", reflect.TypeOf((*MockPMAPIProvider)(nil).EmptyFolder), arg0, arg1)
}
// GetAttachment mocks base method
func (m *MockPMAPIProvider) GetAttachment(arg0 string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAttachment", arg0)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAttachment indicates an expected call of GetAttachment
func (mr *MockPMAPIProviderMockRecorder) GetAttachment(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockPMAPIProvider)(nil).GetAttachment), arg0)
}
// GetContactByID mocks base method
func (m *MockPMAPIProvider) GetContactByID(arg0 string) (pmapi.Contact, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContactByID", arg0)
ret0, _ := ret[0].(pmapi.Contact)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetContactByID indicates an expected call of GetContactByID
func (mr *MockPMAPIProviderMockRecorder) GetContactByID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactByID", reflect.TypeOf((*MockPMAPIProvider)(nil).GetContactByID), arg0)
}
// GetContactEmailByEmail mocks base method
func (m *MockPMAPIProvider) GetContactEmailByEmail(arg0 string, arg1, arg2 int) ([]pmapi.ContactEmail, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContactEmailByEmail", arg0, arg1, arg2)
ret0, _ := ret[0].([]pmapi.ContactEmail)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetContactEmailByEmail indicates an expected call of GetContactEmailByEmail
func (mr *MockPMAPIProviderMockRecorder) GetContactEmailByEmail(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactEmailByEmail", reflect.TypeOf((*MockPMAPIProvider)(nil).GetContactEmailByEmail), arg0, arg1, arg2)
}
// GetEvent mocks base method
func (m *MockPMAPIProvider) GetEvent(arg0 string) (*pmapi.Event, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEvent", arg0)
ret0, _ := ret[0].(*pmapi.Event)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetEvent indicates an expected call of GetEvent
func (mr *MockPMAPIProviderMockRecorder) GetEvent(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvent", reflect.TypeOf((*MockPMAPIProvider)(nil).GetEvent), arg0)
}
// GetMailSettings mocks base method
func (m *MockPMAPIProvider) GetMailSettings() (pmapi.MailSettings, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMailSettings")
ret0, _ := ret[0].(pmapi.MailSettings)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMailSettings indicates an expected call of GetMailSettings
func (mr *MockPMAPIProviderMockRecorder) GetMailSettings() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailSettings", reflect.TypeOf((*MockPMAPIProvider)(nil).GetMailSettings))
}
// GetMessage mocks base method
func (m *MockPMAPIProvider) GetMessage(arg0 string) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMessage", arg0)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMessage indicates an expected call of GetMessage
func (mr *MockPMAPIProviderMockRecorder) GetMessage(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockPMAPIProvider)(nil).GetMessage), arg0)
}
// GetPublicKeysForEmail mocks base method
func (m *MockPMAPIProvider) GetPublicKeysForEmail(arg0 string) ([]pmapi.PublicKey, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPublicKeysForEmail", arg0)
ret0, _ := ret[0].([]pmapi.PublicKey)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetPublicKeysForEmail indicates an expected call of GetPublicKeysForEmail
func (mr *MockPMAPIProviderMockRecorder) GetPublicKeysForEmail(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeysForEmail", reflect.TypeOf((*MockPMAPIProvider)(nil).GetPublicKeysForEmail), arg0)
}
// Import mocks base method
func (m *MockPMAPIProvider) Import(arg0 []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Import", arg0)
ret0, _ := ret[0].([]*pmapi.ImportMsgRes)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Import indicates an expected call of Import
func (mr *MockPMAPIProviderMockRecorder) Import(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*MockPMAPIProvider)(nil).Import), arg0)
}
// KeyRingForAddressID mocks base method
func (m *MockPMAPIProvider) KeyRingForAddressID(arg0 string) *crypto.KeyRing {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
return ret0
}
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID
func (mr *MockPMAPIProviderMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockPMAPIProvider)(nil).KeyRingForAddressID), arg0)
}
// LabelMessages mocks base method
func (m *MockPMAPIProvider) LabelMessages(arg0 []string, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LabelMessages", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// LabelMessages indicates an expected call of LabelMessages
func (mr *MockPMAPIProviderMockRecorder) LabelMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).LabelMessages), arg0, arg1)
}
// ListLabels mocks base method
func (m *MockPMAPIProvider) ListLabels() ([]*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListLabels")
ret0, _ := ret[0].([]*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListLabels indicates an expected call of ListLabels
func (mr *MockPMAPIProviderMockRecorder) ListLabels() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLabels", reflect.TypeOf((*MockPMAPIProvider)(nil).ListLabels))
}
// ListMessages mocks base method
func (m *MockPMAPIProvider) ListMessages(arg0 *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListMessages", arg0)
ret0, _ := ret[0].([]*pmapi.Message)
ret1, _ := ret[1].(int)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListMessages indicates an expected call of ListMessages
func (mr *MockPMAPIProviderMockRecorder) ListMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).ListMessages), arg0)
}
// Logout mocks base method
func (m *MockPMAPIProvider) Logout() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout")
ret0, _ := ret[0].(error)
return ret0
}
// Logout indicates an expected call of Logout
func (mr *MockPMAPIProviderMockRecorder) Logout() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockPMAPIProvider)(nil).Logout))
}
// MarkMessagesRead mocks base method
func (m *MockPMAPIProvider) MarkMessagesRead(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarkMessagesRead", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// MarkMessagesRead indicates an expected call of MarkMessagesRead
func (mr *MockPMAPIProviderMockRecorder) MarkMessagesRead(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesRead", reflect.TypeOf((*MockPMAPIProvider)(nil).MarkMessagesRead), arg0)
}
// MarkMessagesUnread mocks base method
func (m *MockPMAPIProvider) MarkMessagesUnread(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarkMessagesUnread", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// MarkMessagesUnread indicates an expected call of MarkMessagesUnread
func (mr *MockPMAPIProviderMockRecorder) MarkMessagesUnread(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesUnread", reflect.TypeOf((*MockPMAPIProvider)(nil).MarkMessagesUnread), arg0)
}
// ReportBugWithEmailClient mocks base method
func (m *MockPMAPIProvider) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportBugWithEmailClient", arg0, arg1, arg2, arg3, arg4, arg5, arg6)
ret0, _ := ret[0].(error)
return ret0
}
// ReportBugWithEmailClient indicates an expected call of ReportBugWithEmailClient
func (mr *MockPMAPIProviderMockRecorder) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBugWithEmailClient", reflect.TypeOf((*MockPMAPIProvider)(nil).ReportBugWithEmailClient), arg0, arg1, arg2, arg3, arg4, arg5, arg6)
}
// SendMessage mocks base method
func (m *MockPMAPIProvider) SendMessage(arg0 string, arg1 *pmapi.SendMessageReq) (*pmapi.Message, *pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendMessage", arg0, arg1)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(*pmapi.Message)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// SendMessage indicates an expected call of SendMessage
func (mr *MockPMAPIProviderMockRecorder) SendMessage(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockPMAPIProvider)(nil).SendMessage), arg0, arg1)
}
// SendSimpleMetric mocks base method
func (m *MockPMAPIProvider) SendSimpleMetric(arg0, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendSimpleMetric", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// SendSimpleMetric indicates an expected call of SendSimpleMetric
func (mr *MockPMAPIProviderMockRecorder) SendSimpleMetric(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendSimpleMetric", reflect.TypeOf((*MockPMAPIProvider)(nil).SendSimpleMetric), arg0, arg1, arg2)
}
// SetAuths mocks base method
func (m *MockPMAPIProvider) SetAuths(arg0 chan<- *pmapi.Auth) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetAuths", arg0)
}
// SetAuths indicates an expected call of SetAuths
func (mr *MockPMAPIProviderMockRecorder) SetAuths(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAuths", reflect.TypeOf((*MockPMAPIProvider)(nil).SetAuths), arg0)
}
// UnlabelMessages mocks base method
func (m *MockPMAPIProvider) UnlabelMessages(arg0 []string, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnlabelMessages", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UnlabelMessages indicates an expected call of UnlabelMessages
func (mr *MockPMAPIProviderMockRecorder) UnlabelMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlabelMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).UnlabelMessages), arg0, arg1)
}
// Unlock mocks base method
func (m *MockPMAPIProvider) Unlock(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unlock", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Unlock indicates an expected call of Unlock
func (mr *MockPMAPIProviderMockRecorder) Unlock(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockPMAPIProvider)(nil).Unlock), arg0)
}
// UnlockAddresses mocks base method
func (m *MockPMAPIProvider) UnlockAddresses(arg0 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnlockAddresses", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UnlockAddresses indicates an expected call of UnlockAddresses
func (mr *MockPMAPIProviderMockRecorder) UnlockAddresses(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlockAddresses", reflect.TypeOf((*MockPMAPIProvider)(nil).UnlockAddresses), arg0)
}
// UpdateLabel mocks base method
func (m *MockPMAPIProvider) UpdateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateLabel", arg0)
ret0, _ := ret[0].(*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateLabel indicates an expected call of UpdateLabel
func (mr *MockPMAPIProviderMockRecorder) UpdateLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).UpdateLabel), arg0)
}
// UpdateUser mocks base method
func (m *MockPMAPIProvider) UpdateUser() (*pmapi.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser")
ret0, _ := ret[0].(*pmapi.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser
func (mr *MockPMAPIProviderMockRecorder) UpdateUser() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockPMAPIProvider)(nil).UpdateUser))
}
// MockCredentialsStorer is a mock of CredentialsStorer interface
type MockCredentialsStorer struct {
ctrl *gomock.Controller
recorder *MockCredentialsStorerMockRecorder
}
// MockCredentialsStorerMockRecorder is the mock recorder for MockCredentialsStorer
type MockCredentialsStorerMockRecorder struct {
mock *MockCredentialsStorer
}
// NewMockCredentialsStorer creates a new mock instance
func NewMockCredentialsStorer(ctrl *gomock.Controller) *MockCredentialsStorer {
mock := &MockCredentialsStorer{ctrl: ctrl}
mock.recorder = &MockCredentialsStorerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockCredentialsStorer) EXPECT() *MockCredentialsStorerMockRecorder {
return m.recorder
}
// Add mocks base method
func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3 string, arg4 []string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Add", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(*credentials.Credentials)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Add indicates an expected call of Add
func (mr *MockCredentialsStorerMockRecorder) Add(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockCredentialsStorer)(nil).Add), arg0, arg1, arg2, arg3, arg4)
}
// Delete mocks base method
func (m *MockCredentialsStorer) Delete(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockCredentialsStorerMockRecorder) Delete(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCredentialsStorer)(nil).Delete), arg0)
}
// Get mocks base method
func (m *MockCredentialsStorer) Get(arg0 string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(*credentials.Credentials)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockCredentialsStorerMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCredentialsStorer)(nil).Get), arg0)
}
// List mocks base method
func (m *MockCredentialsStorer) List() ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockCredentialsStorerMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCredentialsStorer)(nil).List))
}
// Logout mocks base method
func (m *MockCredentialsStorer) Logout(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Logout indicates an expected call of Logout
func (mr *MockCredentialsStorerMockRecorder) Logout(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockCredentialsStorer)(nil).Logout), arg0)
}
// SwitchAddressMode mocks base method
func (m *MockCredentialsStorer) SwitchAddressMode(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwitchAddressMode", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SwitchAddressMode indicates an expected call of SwitchAddressMode
func (mr *MockCredentialsStorerMockRecorder) SwitchAddressMode(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwitchAddressMode", reflect.TypeOf((*MockCredentialsStorer)(nil).SwitchAddressMode), arg0)
}
// UpdateEmails mocks base method
func (m *MockCredentialsStorer) UpdateEmails(arg0 string, arg1 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateEmails", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateEmails indicates an expected call of UpdateEmails
func (mr *MockCredentialsStorerMockRecorder) UpdateEmails(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmails", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateEmails), arg0, arg1)
}
// UpdateToken mocks base method
func (m *MockCredentialsStorer) UpdateToken(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateToken", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateToken indicates an expected call of UpdateToken
func (mr *MockCredentialsStorerMockRecorder) UpdateToken(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateToken", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateToken), arg0, arg1)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -15,12 +15,20 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Fri Jan 22 11:01:06 AM CET 2021'. DO NOT EDIT. // Code generated by ./release-notes.sh at Mon Apr 6 08:14:14 CEST 2020. DO NOT EDIT.
package bridge package bridge
const ReleaseNotes = ` const ReleaseNotes = `NOTE: We recommend to reconfigure your email client after upgrading to ensure the best results with the new draft folder support
• Faster and more resilient mail synchronization process, especially for large mailboxes
• Added "Alternate Routing" feature to mitigate blocking of Proton Servers
• Added synchronization of draft folder
• Improved event handling when there are frequent changes
• Security improvements for loading dependent libraries
• Minor UI & API communication tweaks
` `
const ReleaseFixedBugs = `• Fixed sending error caused by inconsistent use of upper and lower case in senders email address const ReleaseFixedBugs = `• Fixed rare case of sending the same message multiple times in Outlook
• Fixed bug in macOS update process; available from next update
` `

View File

@ -1,72 +0,0 @@
// 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
import (
"fmt"
"path/filepath"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
type storeFactory struct {
cache Cacher
sentryReporter *sentry.Reporter
panicHandler users.PanicHandler
clientManager users.ClientManager
eventListener listener.Listener
storeCache *store.Cache
}
func newStoreFactory(
cache Cacher,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler,
clientManager users.ClientManager,
eventListener listener.Listener,
) *storeFactory {
return &storeFactory{
cache: cache,
sentryReporter: sentryReporter,
panicHandler: panicHandler,
clientManager: clientManager,
eventListener: eventListener,
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.cache.GetDBDir(), user.ID())
return store.New(f.sentryReporter, 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.cache.GetDBDir(), userID)
return store.RemoveStore(f.storeCache, storePath, userID)
}
// getUserStorePath returns the file path of the store database for the given userID.
func getUserStorePath(storeDir string, userID string) (path string) {
fileName := fmt.Sprintf("mailbox-%v.db", userID)
return filepath.Join(storeDir, fileName)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -18,34 +18,88 @@
package bridge package bridge
import ( import (
"github.com/Masterminds/semver/v3" "io"
"github.com/ProtonMail/proton-bridge/internal/updater" pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" // mockgen needs this to be given an explicit import name
) )
type Locator interface { type Configer interface {
Clear() error ClearData() error
ClearUpdates() error
}
type Cacher interface {
GetIMAPCachePath() string
GetDBDir() string GetDBDir() string
GetIMAPCachePath() string
GetAPIConfig() *pmapi.ClientConfig
} }
type SettingsProvider interface { type PreferenceProvider interface {
Get(key string) string Get(key string) string
Set(key string, value string)
GetBool(key string) bool GetBool(key string) bool
SetBool(key string, val bool) GetInt(key string) int
Set(key string, value string)
} }
type Updater interface { type PanicHandler interface {
Check() (updater.VersionInfo, error) HandlePanic()
IsDowngrade(updater.VersionInfo) bool
InstallUpdate(updater.VersionInfo) error
} }
type Versioner interface { type PMAPIProviderFactory func(string) PMAPIProvider
RemoveOtherVersions(*semver.Version) error
type PMAPIProvider interface {
SetAuths(auths chan<- *pmapi.Auth)
Auth(username, password string, info *pmapi.AuthInfo) (*pmapi.Auth, error)
AuthInfo(username string) (*pmapi.AuthInfo, error)
AuthRefresh(token string) (*pmapi.Auth, error)
Unlock(mailboxPassword string) (kr *pmcrypto.KeyRing, err error)
UnlockAddresses(passphrase []byte) error
CurrentUser() (*pmapi.User, error)
UpdateUser() (*pmapi.User, error)
Addresses() pmapi.AddressList
Logout() error
GetEvent(eventID string) (*pmapi.Event, error)
CountMessages(addressID string) ([]*pmapi.MessagesCount, error)
ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Message, int, error)
GetMessage(apiID string) (*pmapi.Message, error)
Import([]*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error)
DeleteMessages(apiIDs []string) error
LabelMessages(apiIDs []string, labelID string) error
UnlabelMessages(apiIDs []string, labelID string) error
MarkMessagesRead(apiIDs []string) error
MarkMessagesUnread(apiIDs []string) error
ListLabels() ([]*pmapi.Label, error)
CreateLabel(label *pmapi.Label) (*pmapi.Label, error)
UpdateLabel(label *pmapi.Label) (*pmapi.Label, error)
DeleteLabel(labelID string) error
EmptyFolder(labelID string, addressID string) error
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
SendSimpleMetric(category, action, label string) error
Auth2FA(twoFactorCode string, auth *pmapi.Auth) (*pmapi.Auth2FA, error)
GetMailSettings() (pmapi.MailSettings, error)
GetContactEmailByEmail(string, int, int) ([]pmapi.ContactEmail, error)
GetContactByID(string) (pmapi.Contact, error)
DecryptAndVerifyCards([]pmapi.Card) ([]pmapi.Card, error)
GetPublicKeysForEmail(string) ([]pmapi.PublicKey, bool, error)
SendMessage(string, *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err error)
CreateDraft(m *pmapi.Message, parent string, action int) (created *pmapi.Message, err error)
CreateAttachment(att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error)
KeyRingForAddressID(string) (kr *pmcrypto.KeyRing)
GetAttachment(id string) (att io.ReadCloser, err error)
}
type CredentialsStorer interface {
List() (userIDs []string, err error)
Add(userID, userName, apiToken, mailboxPassword string, emails []string) (*credentials.Credentials, error)
Get(userID string) (*credentials.Credentials, error)
SwitchAddressMode(userID string) error
UpdateEmails(userID string, emails []string) error
UpdateToken(userID, apiToken string) error
Logout(userID string) error
Delete(userID string) error
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -15,51 +15,61 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package users package bridge
import ( import (
"fmt"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" logrus "github.com/sirupsen/logrus"
) )
// ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from the app. // ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from bridge.
var ErrLoggedOutUser = errors.New("account is logged out, use the app to login again") var ErrLoggedOutUser = errors.New("bridge account is logged out, use bridge to login again")
// User is a struct on top of API client and credentials store. // User is a struct on top of API client and credentials store.
type User struct { type User struct {
log *logrus.Entry log *logrus.Entry
panicHandler PanicHandler panicHandler PanicHandler
listener listener.Listener listener listener.Listener
clientManager ClientManager apiClient PMAPIProvider
credStorer CredentialsStorer credStorer CredentialsStorer
storeFactory StoreMaker imapUpdatesChannel chan interface{}
store *store.Store store *store.Store
storeCache *store.Cache
storePath string
userID string userID string
creds *credentials.Credentials creds *credentials.Credentials
lock sync.RWMutex lock sync.RWMutex
isAuthorized bool authChannel chan *pmapi.Auth
hasAPIAuth bool
unlockingKeyringLock sync.Mutex
wasKeyringUnlocked bool
} }
// newUser creates a new user. // newUser creates a new bridge user.
func newUser( func newUser(
panicHandler PanicHandler, panicHandler PanicHandler,
userID string, userID string,
eventListener listener.Listener, eventListener listener.Listener,
credStorer CredentialsStorer, credStorer CredentialsStorer,
clientManager ClientManager, apiClient PMAPIProvider,
storeFactory StoreMaker, storeCache *store.Cache,
storeDir string,
) (u *User, err error) { ) (u *User, err error) {
log := log.WithField("user", userID) log := log.WithField("user", userID)
log.Debug("Creating or loading user") log.Debug("Creating or loading user")
@ -74,8 +84,9 @@ func newUser(
panicHandler: panicHandler, panicHandler: panicHandler,
listener: eventListener, listener: eventListener,
credStorer: credStorer, credStorer: credStorer,
clientManager: clientManager, apiClient: apiClient,
storeFactory: storeFactory, storeCache: storeCache,
storePath: getUserStorePath(storeDir, userID),
userID: userID, userID: userID,
creds: creds, creds: creds,
} }
@ -83,17 +94,19 @@ func newUser(
return return
} }
func (u *User) client() pmapi.Client { // init initialises a bridge user. This includes reloading its credentials from the credentials store
return u.clientManager.GetClient(u.userID)
}
// init initialises a user. This includes reloading its credentials from the credentials store
// (such as when logging out and back in, you need to reload the credentials because the new credentials will // (such as when logging out and back in, you need to reload the credentials because the new credentials will
// have the apitoken and password), authorising the user against the api, loading the user store (creating a new one // have the apitoken and password), authorising the user against the api, loading the user store (creating a new one
// if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if // if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if
// something in the store changed). // something in the store changed).
func (u *User) init() (err error) { func (u *User) init(idleUpdates chan interface{}, apiClient PMAPIProvider) (err error) {
u.log.Info("Initialising user") // If this is an existing user, we still need a new api client to get a new refresh token.
// If it's a new user, doesn't matter really; this is basically a noop in this case.
u.apiClient = apiClient
u.unlockingKeyringLock.Lock()
u.wasKeyringUnlocked = false
u.unlockingKeyringLock.Unlock()
// Reload the user's credentials (if they log out and back in we need the new // Reload the user's credentials (if they log out and back in we need the new
// version with the apitoken and mailbox password). // version with the apitoken and mailbox password).
@ -103,8 +116,17 @@ func (u *User) init() (err error) {
} }
u.creds = creds u.creds = creds
// Set up the auth channel on which auths from the api client are sent.
u.authChannel = make(chan *pmapi.Auth)
u.apiClient.SetAuths(u.authChannel)
u.hasAPIAuth = false
go func() {
defer u.panicHandler.HandlePanic()
u.watchAPIClientAuths()
}()
// Try to authorise the user if they aren't already authorised. // Try to authorise the user if they aren't already authorised.
// Note: we still allow users to set up accounts if the internet is off. // Note: we still allow users to set up bridge if the internet is off.
if authErr := u.authorizeIfNecessary(false); authErr != nil { if authErr := u.authorizeIfNecessary(false); authErr != nil {
switch errors.Cause(authErr) { switch errors.Cause(authErr) {
case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser: case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser:
@ -125,21 +147,32 @@ func (u *User) init() (err error) {
} }
u.store = nil u.store = nil
} }
store, err := u.storeFactory.New(u) store, err := store.New(u.panicHandler, u, u.apiClient, u.listener, u.storePath, u.storeCache)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create store") return errors.Wrap(err, "failed to create store")
} }
u.store = store u.store = store
// Save the imap updates channel here so it can be set later when imap connects.
u.imapUpdatesChannel = idleUpdates
return err return err
} }
func (u *User) SetIMAPIdleUpdateChannel() {
if u.store == nil {
return
}
u.store.SetIMAPUpdateChannel(u.imapUpdatesChannel)
}
// authorizeIfNecessary checks whether user is logged in and is connected to api auth channel. // authorizeIfNecessary checks whether user is logged in and is connected to api auth channel.
// If user is not already connected to the api auth channel (for example there was no internet during start), // If user is not already connected to the api auth channel (for example there was no internet during start),
// it tries to connect it. // it tries to connect it. See `connectToAuthChannel` for more info.
func (u *User) authorizeIfNecessary(emitEvent bool) (err error) { func (u *User) authorizeIfNecessary(emitEvent bool) (err error) {
// If user is connected and has an auth channel, then perfect, nothing to do here. // If user is connected and has an auth channel, then perfect, nothing to do here.
if u.creds.IsConnected() && u.isAuthorized { if u.creds.IsConnected() && u.HasAPIAuth() {
// The keyring unlock is triggered here to resolve state where apiClient // The keyring unlock is triggered here to resolve state where apiClient
// is authenticated (we have auth token) but it was not possible to download // is authenticated (we have auth token) but it was not possible to download
// and unlock the keys (internet not reachable). // and unlock the keys (internet not reachable).
@ -152,7 +185,12 @@ func (u *User) authorizeIfNecessary(emitEvent bool) (err error) {
u.log.WithError(err).Error("Could not authorize and unlock user") u.log.WithError(err).Error("Could not authorize and unlock user")
switch errors.Cause(err) { switch errors.Cause(err) {
case pmapi.ErrUpgradeApplication, pmapi.ErrAPINotReachable: // Ignore these errors. case pmapi.ErrUpgradeApplication:
u.listener.Emit(events.UpgradeApplicationEvent, "")
case pmapi.ErrAPINotReachable:
u.listener.Emit(events.InternetOffEvent, "")
default: default:
if errLogout := u.credStorer.Logout(u.userID); errLogout != nil { if errLogout := u.credStorer.Logout(u.userID); errLogout != nil {
u.log.WithField("err", errLogout).Error("Could not log user out from credentials store") u.log.WithField("err", errLogout).Error("Could not log user out from credentials store")
@ -171,14 +209,22 @@ func (u *User) authorizeIfNecessary(emitEvent bool) (err error) {
// unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked. // unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked.
func (u *User) unlockIfNecessary() error { func (u *User) unlockIfNecessary() error {
if u.client().IsUnlocked() { u.unlockingKeyringLock.Lock()
defer u.unlockingKeyringLock.Unlock()
if u.wasKeyringUnlocked {
return nil return nil
} }
if err := u.client().Unlock([]byte(u.creds.MailboxPassword)); err != nil { if _, err := u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
return errors.Wrap(err, "failed to unlock user") return errors.Wrap(err, "failed to unlock user")
} }
if err := u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return errors.Wrap(err, "failed to unlock user addresses")
}
u.wasKeyringUnlocked = true
return nil return nil
} }
@ -190,28 +236,49 @@ func (u *User) authorizeAndUnlock() (err error) {
return nil return nil
} }
if _, err := u.client().AuthRefresh(u.creds.APIToken); err != nil { auth, err := u.apiClient.AuthRefresh(u.creds.APIToken)
if err != nil {
return errors.Wrap(err, "failed to refresh API auth") return errors.Wrap(err, "failed to refresh API auth")
} }
u.authChannel <- auth
if err := u.client().Unlock([]byte(u.creds.MailboxPassword)); err != nil { if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
return errors.Wrap(err, "failed to unlock user") return errors.Wrap(err, "failed to unlock user")
} }
if err = u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return errors.Wrap(err, "failed to unlock user addresses")
}
return nil return nil
} }
func (u *User) updateAuthToken(auth *pmapi.Auth) { // See `connectToAPIClientAuthChannel` for more info.
u.log.Debug("User received auth") func (u *User) watchAPIClientAuths() {
for auth := range u.authChannel {
if err := u.credStorer.UpdateToken(u.userID, auth.GenToken()); err != nil { if auth != nil {
u.log.WithError(err).Error("Failed to update refresh token in credentials store") newRefreshToken := auth.UID() + ":" + auth.RefreshToken
return u.updateAPIToken(newRefreshToken)
u.hasAPIAuth = true
} else if err := u.logout(); err != nil {
u.log.WithError(err).Error("Cannot logout user after receiving empty auth from API")
}
}
} }
u.refreshFromCredentials() // updateAPIToken is helper for updating the token in keychain. It's not supposed to be
// called directly from other parts of the code--only from `watchAPIClientAuths`.
func (u *User) updateAPIToken(newRefreshToken string) {
u.lock.Lock()
defer u.lock.Unlock()
u.isAuthorized = true u.log.Info("Saving refresh token")
if err := u.credStorer.UpdateToken(u.userID, newRefreshToken); err != nil {
u.log.WithError(err).Error("Cannot update refresh token in credentials store")
} else {
u.refreshFromCredentials()
}
} }
// clearStore removes the database. // clearStore removes the database.
@ -224,7 +291,7 @@ func (u *User) clearStore() error {
} }
} else { } else {
u.log.Warn("Store is not initialized: cleaning up store files manually") u.log.Warn("Store is not initialized: cleaning up store files manually")
if err := u.storeFactory.Remove(u.userID); err != nil { if err := store.RemoveStore(u.storeCache, u.storePath, u.userID); err != nil {
return errors.Wrap(err, "failed to remove store manually") return errors.Wrap(err, "failed to remove store manually")
} }
} }
@ -244,11 +311,17 @@ func (u *User) closeStore() error {
return nil return nil
} }
// getUserStorePath returns the file path of the store database for the given userID.
func getUserStorePath(storeDir string, userID string) (path string) {
fileName := fmt.Sprintf("mailbox-%v.db", userID)
return filepath.Join(storeDir, fileName)
}
// GetTemporaryPMAPIClient returns an authorised PMAPI client. // GetTemporaryPMAPIClient returns an authorised PMAPI client.
// Do not use! It's only for backward compatibility of old SMTP and IMAP implementations. // Do not use! It's only for backward compatibility of old SMTP and IMAP implementations.
// After proper refactor of SMTP and IMAP remove this method. // After proper refactor of SMTP and IMAP remove this method.
func (u *User) GetTemporaryPMAPIClient() pmapi.Client { func (u *User) GetTemporaryPMAPIClient() PMAPIProvider {
return u.client() return u.apiClient
} }
// ID returns the user's userID. // ID returns the user's userID.
@ -340,17 +413,14 @@ func (u *User) GetAddressID(address string) (id string, err error) {
u.lock.RLock() u.lock.RLock()
defer u.lock.RUnlock() defer u.lock.RUnlock()
if u.store != nil {
address = strings.ToLower(address) address = strings.ToLower(address)
return u.store.GetAddressID(address)
if u.store == nil {
err = errors.New("store is not initialised")
return
} }
addresses := u.client().Addresses() return u.store.GetAddressID(address)
pmapiAddress := addresses.ByEmail(address)
if pmapiAddress != nil {
return pmapiAddress.ID, nil
}
return "", errors.New("address not found")
} }
// GetBridgePassword returns bridge password. This is not a password of the PM // GetBridgePassword returns bridge password. This is not a password of the PM
@ -364,7 +434,7 @@ func (u *User) GetBridgePassword() string {
} }
// CheckBridgeLogin checks whether the user is logged in and the bridge // CheckBridgeLogin checks whether the user is logged in and the bridge
// IMAP/SMTP password is correct. // password is correct.
func (u *User) CheckBridgeLogin(password string) error { func (u *User) CheckBridgeLogin(password string) error {
if isApplicationOutdated { if isApplicationOutdated {
u.listener.Emit(events.UpgradeApplicationEvent, "") u.listener.Emit(events.UpgradeApplicationEvent, "")
@ -392,16 +462,20 @@ func (u *User) UpdateUser() error {
return errors.Wrap(err, "cannot update user") return errors.Wrap(err, "cannot update user")
} }
_, err := u.client().UpdateUser() _, err := u.apiClient.UpdateUser()
if err != nil { if err != nil {
return err return err
} }
if err = u.client().ReloadKeys([]byte(u.creds.MailboxPassword)); err != nil { if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
return errors.Wrap(err, "failed to reload keys") return err
} }
emails := u.client().Addresses().ActiveEmails() if err := u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return err
}
emails := u.apiClient.Addresses().ActiveEmails()
if err := u.credStorer.UpdateEmails(u.userID, emails); err != nil { if err := u.credStorer.UpdateEmails(u.userID, emails); err != nil {
return err return err
} }
@ -418,7 +492,7 @@ func (u *User) SwitchAddressMode() (err error) {
u.lock.Lock() u.lock.Lock()
defer u.lock.Unlock() defer u.lock.Unlock()
u.CloseAllConnections() u.closeAllConnections()
if u.store == nil { if u.store == nil {
err = errors.New("store is not initialised") err = errors.New("store is not initialised")
@ -445,21 +519,16 @@ func (u *User) SwitchAddressMode() (err error) {
} }
// logout is the same as Logout, but for internal purposes (logged out from // logout is the same as Logout, but for internal purposes (logged out from
// the server) which emits LogoutEvent to notify other parts of the app. // the server) which emits LogoutEvent to notify other parts of the Bridge.
func (u *User) logout() error { func (u *User) logout() error {
u.lock.Lock() u.lock.Lock()
wasConnected := u.creds.IsConnected() wasConnected := u.creds.IsConnected()
u.lock.Unlock() u.lock.Unlock()
err := u.Logout() err := u.Logout()
if wasConnected { if wasConnected {
u.listener.Emit(events.LogoutEvent, u.userID) u.listener.Emit(events.LogoutEvent, u.userID)
u.listener.Emit(events.UserRefreshEvent, u.userID) u.listener.Emit(events.UserRefreshEvent, u.userID)
} }
u.isAuthorized = false
return err return err
} }
@ -475,7 +544,22 @@ func (u *User) Logout() (err error) {
return return
} }
u.client().Logout() u.unlockingKeyringLock.Lock()
u.wasKeyringUnlocked = false
u.unlockingKeyringLock.Unlock()
if err = u.apiClient.Logout(); err != nil {
u.log.WithError(err).Warn("Could not log user out from API client")
}
u.apiClient.SetAuths(nil)
// Logout needs to stop auth channel so when user logs back in
// it can register again with new client.
// Note: be careful to not close channel twice.
if u.authChannel != nil {
close(u.authChannel)
u.authChannel = nil
}
if err = u.credStorer.Logout(u.userID); err != nil { if err = u.credStorer.Logout(u.userID); err != nil {
u.log.WithError(err).Warn("Could not log user out from credentials store") u.log.WithError(err).Warn("Could not log user out from credentials store")
@ -490,8 +574,7 @@ func (u *User) Logout() (err error) {
// Do not close whole store, just event loop. Some information might be needed offline (e.g. addressID) // Do not close whole store, just event loop. Some information might be needed offline (e.g. addressID)
u.closeEventLoop() u.closeEventLoop()
u.CloseAllConnections() u.closeAllConnections()
runtime.GC() runtime.GC()
return err return err
@ -499,7 +582,7 @@ func (u *User) Logout() (err error) {
func (u *User) refreshFromCredentials() { func (u *User) refreshFromCredentials() {
if credentials, err := u.credStorer.Get(u.userID); err != nil { if credentials, err := u.credStorer.Get(u.userID); err != nil {
log.WithError(err).Error("Cannot refresh user credentials") log.Error("Cannot update credentials: ", err)
} else { } else {
u.creds = credentials u.creds = credentials
} }
@ -513,14 +596,14 @@ func (u *User) closeEventLoop() {
u.store.CloseEventLoop() u.store.CloseEventLoop()
} }
// CloseAllConnections calls CloseConnection for all users addresses. // closeAllConnections calls CloseConnection for all users addresses.
func (u *User) CloseAllConnections() { func (u *User) closeAllConnections() {
for _, address := range u.creds.EmailList() { for _, address := range u.creds.EmailList() {
u.CloseConnection(address) u.CloseConnection(address)
} }
if u.store != nil { if u.store != nil {
u.store.SetChangeNotifier(nil) u.store.SetIMAPUpdateChannel(nil)
} }
} }
@ -532,3 +615,7 @@ func (u *User) CloseConnection(address string) {
func (u *User) GetStore() *store.Store { func (u *User) GetStore() *store.Store {
return u.store return u.store
} }
func (u *User) HasAPIAuth() bool {
return u.hasAPIAuth
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package users package bridge
import ( import (
"testing" "testing"
@ -34,22 +34,16 @@ func TestUpdateUser(t *testing.T) {
user := testNewUser(m) user := testNewUser(m)
defer cleanUpUserData(user) defer cleanUpUserData(user)
gomock.InOrder( m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().IsUnlocked().Return(false), m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().UpdateUser().Return(nil, nil), m.pmapiClient.EXPECT().UpdateUser().Return(nil, nil)
m.pmapiClient.EXPECT().ReloadKeys([]byte(testCredentials.MailboxPassword)).Return(nil), m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}), m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email})
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
)
gomock.InOrder(
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).MaxTimes(1),
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1),
)
assert.NoError(t, user.UpdateUser()) assert.NoError(t, user.UpdateUser())
@ -111,12 +105,9 @@ func TestLogoutUser(t *testing.T) {
user := testNewUserForLogout(m) user := testNewUserForLogout(m)
defer cleanUpUserData(user) defer cleanUpUserData(user)
gomock.InOrder( m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().Logout().Return(), m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil), m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := user.Logout() err := user.Logout()
@ -133,12 +124,10 @@ func TestLogoutUserFailsLogout(t *testing.T) {
user := testNewUserForLogout(m) user := testNewUserForLogout(m)
defer cleanUpUserData(user) defer cleanUpUserData(user)
gomock.InOrder( m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().Logout().Return(), m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed"))
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed")), m.credentialsStore.EXPECT().Delete("user").Return(nil)
m.credentialsStore.EXPECT().Delete("user").Return(nil), m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := user.Logout() err := user.Logout()
@ -146,17 +135,15 @@ func TestLogoutUserFailsLogout(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestCheckBridgeLoginOK(t *testing.T) { func TestCheckBridgeLogin(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
user := testNewUser(m) user := testNewUser(m)
defer cleanUpUserData(user) defer cleanUpUserData(user)
gomock.InOrder( m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().IsUnlocked().Return(false), m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
)
err := user.CheckBridgeLogin(testCredentials.BridgePassword) err := user.CheckBridgeLogin(testCredentials.BridgePassword)
@ -165,28 +152,6 @@ func TestCheckBridgeLoginOK(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestCheckBridgeLoginTwiceOK(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
gomock.InOrder(
m.pmapiClient.EXPECT().IsUnlocked().Return(false),
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
m.pmapiClient.EXPECT().IsUnlocked().Return(true),
)
err := user.CheckBridgeLogin(testCredentials.BridgePassword)
waitForEvents()
assert.NoError(t, err)
err = user.CheckBridgeLogin(testCredentials.BridgePassword)
waitForEvents()
assert.NoError(t, err)
}
func TestCheckBridgeLoginUpgradeApplication(t *testing.T) { func TestCheckBridgeLoginUpgradeApplication(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -197,12 +162,11 @@ func TestCheckBridgeLoginUpgradeApplication(t *testing.T) {
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "") m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
isApplicationOutdated = true isApplicationOutdated = true
err := user.CheckBridgeLogin("any-pass") err := user.CheckBridgeLogin("any-pass")
waitForEvents() waitForEvents()
assert.Equal(t, pmapi.ErrUpgradeApplication, err)
isApplicationOutdated = false isApplicationOutdated = false
assert.Equal(t, pmapi.ErrUpgradeApplication, err)
} }
func TestCheckBridgeLoginLoggedOut(t *testing.T) { func TestCheckBridgeLoginLoggedOut(t *testing.T) {
@ -210,27 +174,22 @@ func TestCheckBridgeLoginLoggedOut(t *testing.T) {
defer m.ctrl.Finish() defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil) m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
m.pmapiClient.EXPECT().Addresses().Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker) m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
assert.NoError(t, err) _ = user.init(nil, m.pmapiClient)
m.clientManager.EXPECT().GetClient(gomock.Any()).Return(m.pmapiClient).MinTimes(1)
gomock.InOrder(
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized")),
m.pmapiClient.EXPECT().Addresses().Return(nil),
)
err = user.init()
assert.Error(t, err)
defer cleanUpUserData(user) defer cleanUpUserData(user)
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user") m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
err = user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword) err := user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword)
waitForEvents() waitForEvents()
assert.Equal(t, ErrLoggedOutUser, err)
assert.Equal(t, "bridge account is logged out, use bridge to login again", err.Error())
} }
func TestCheckBridgeLoginBadPassword(t *testing.T) { func TestCheckBridgeLoginBadPassword(t *testing.T) {
@ -240,10 +199,8 @@ func TestCheckBridgeLoginBadPassword(t *testing.T) {
user := testNewUser(m) user := testNewUser(m)
defer cleanUpUserData(user) defer cleanUpUserData(user)
gomock.InOrder( m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().IsUnlocked().Return(false), m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
)
err := user.CheckBridgeLogin("wrong!") err := user.CheckBridgeLogin("wrong!")
waitForEvents() waitForEvents()

View File

@ -0,0 +1,188 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
a "github.com/stretchr/testify/assert"
)
func TestNewUserNoCredentialsStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("fail"))
_, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
a.Error(t, err)
}
func TestNewUserBridgeOutdated(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil).AnyTimes()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrUpgradeApplication).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "").AnyTimes()
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrUpgradeApplication)
m.pmapiClient.EXPECT().Addresses().Return(nil)
checkNewUser(m)
}
func TestNewUserNoInternetConnection(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.InternetOffEvent, "").AnyTimes()
m.pmapiClient.EXPECT().Addresses().Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrAPINotReachable)
m.pmapiClient.EXPECT().GetEvent("").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
checkNewUser(m)
}
func TestNewUserAuthRefreshFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token")).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUserUnlockFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, errors.New("bad password"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUserUnlockAddressesFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(errors.New("bad password"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
checkNewUser(m)
}
func checkNewUser(m mocks) {
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
defer cleanUpUserData(user)
_ = user.init(nil, m.pmapiClient)
waitForEvents()
a.Equal(m.t, testCredentials, user.creds)
}
func checkNewUserDisconnected(m mocks) {
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
defer cleanUpUserData(user)
_ = user.init(nil, m.pmapiClient)
waitForEvents()
a.Equal(m.t, testCredentialsDisconnected, user.creds)
}
func _TestUserEventRefreshUpdatesAddresses(t *testing.T) { // nolint[funlen]
a.Fail(t, "not implemented")
}

View File

@ -0,0 +1,113 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testNewUser(m mocks) *User {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
// Expectations for initial sync (when loading existing user from credentials store).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
assert.NoError(m.t, err)
err = user.init(nil, m.pmapiClient)
assert.NoError(m.t, err)
return user
}
func testNewUserForLogout(m mocks) *User {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
// These may or may not be hit depending on how fast the log out happens.
m.pmapiClient.EXPECT().SetAuths(nil).AnyTimes()
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil).AnyTimes()
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}).AnyTimes()
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
assert.NoError(m.t, err)
err = user.init(nil, m.pmapiClient)
assert.NoError(m.t, err)
return user
}
func cleanUpUserData(u *User) {
_ = u.clearStore()
}
func _TestNeverLongStorePath(t *testing.T) { // nolint[unused]
assert.Fail(t, "not implemented")
}
func TestClearStoreWithStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
require.Nil(t, user.store.Close())
user.store = nil
assert.Nil(t, user.clearStore())
}
func TestClearStoreWithoutStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
assert.NotNil(t, user.store)
assert.Nil(t, user.clearStore())
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -15,30 +15,27 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package useragent package bridge
import ( import (
"testing" "fmt"
"runtime"
"github.com/stretchr/testify/assert" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
func TestIsVersionCatalinaOrNewer(t *testing.T) { // UpdateCurrentUserAgent updates user agent on pmapi so each request has this
testData := map[struct{ version string }]bool{ // information in headers for statistic purposes.
{""}: false, func UpdateCurrentUserAgent(bridgeVersion, os, clientName, clientVersion string) {
{"9.0.0"}: false, if os == "" {
{"9.15.0"}: false, os = runtime.GOOS
{"10.13.0"}: false,
{"10.14.0"}: false,
{"10.14.99"}: false,
{"10.15.0"}: true,
{"10.16.0"}: true,
{"11.0.0"}: true,
{"11.1"}: true,
} }
mailClient := "unknown client"
for args, exp := range testData { if clientName != "" {
got := isVersionCatalinaOrNewer(args.version) mailClient = clientName
assert.Equal(t, exp, got, "version %v", args.version) if clientVersion != "" {
mailClient += "/" + clientVersion
} }
} }
pmapi.CurrentUserAgent = fmt.Sprintf("Bridge/%s (%s; %s)", bridgeVersion, os, mailClient)
}

View File

@ -0,0 +1,51 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"runtime"
"testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/stretchr/testify/assert"
)
func TestUpdateCurrentUserAgentGOOS(t *testing.T) {
UpdateCurrentUserAgent("ver", "", "", "")
assert.Equal(t, "Bridge/ver ("+runtime.GOOS+"; unknown client)", pmapi.CurrentUserAgent)
}
func TestUpdateCurrentUserAgentOS(t *testing.T) {
UpdateCurrentUserAgent("ver", "os", "", "")
assert.Equal(t, "Bridge/ver (os; unknown client)", pmapi.CurrentUserAgent)
}
func TestUpdateCurrentUserAgentClientVer(t *testing.T) {
UpdateCurrentUserAgent("ver", "os", "", "cver")
assert.Equal(t, "Bridge/ver (os; unknown client)", pmapi.CurrentUserAgent)
}
func TestUpdateCurrentUserAgentClientName(t *testing.T) {
UpdateCurrentUserAgent("ver", "os", "mail", "")
assert.Equal(t, "Bridge/ver (os; mail)", pmapi.CurrentUserAgent)
}
func TestUpdateCurrentUserAgentClientNameAndVersion(t *testing.T) {
UpdateCurrentUserAgent("ver", "os", "mail", "cver")
assert.Equal(t, "Bridge/ver (os; mail/cver)", pmapi.CurrentUserAgent)
}

View File

@ -1,65 +0,0 @@
// 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)
}

View File

@ -1,70 +0,0 @@
// 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())
}
}

View File

@ -1,90 +0,0 @@
// 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())) //nolint[gosec] G404 It is OK to use weak random number generator here
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")
}

View File

@ -1,53 +0,0 @@
// 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())
}

View File

@ -1,26 +0,0 @@
// 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.
}

View File

@ -1,26 +0,0 @@
// 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?
}

View File

@ -1,155 +0,0 @@
// 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]
return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
}
// 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)
// nolint[gosec]: We need to support older TLS versions for AppleMail and Outlook.
return &tls.Config{
Certificates: []tls.Certificate{c},
ServerName: "127.0.0.1",
ClientAuth: tls.VerifyClientCertIfGiven,
RootCAs: caCertPool,
ClientCAs: caCertPool,
}, nil
}

View File

@ -1,77 +0,0 @@
// 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)
}

View File

@ -1,59 +0,0 @@
// 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 useragent
import (
"fmt"
"regexp"
"runtime"
)
type UserAgent struct {
client, platform string
}
func New() *UserAgent {
return &UserAgent{
client: "",
platform: runtime.GOOS,
}
}
func (ua *UserAgent) SetClient(name, version string) {
ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2"))
}
func (ua *UserAgent) HasClient() bool {
return ua.client != ""
}
func (ua *UserAgent) SetPlatform(platform string) {
ua.platform = platform
}
func (ua *UserAgent) String() string {
var client string
if ua.client != "" {
client = ua.client
} else {
client = "NoClient/0.0.1"
}
return fmt.Sprintf("%v (%v)", client, ua.platform)
}

View File

@ -1,86 +0,0 @@
// 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 useragent
import (
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserAgent(t *testing.T) {
tests := []struct {
name, version, platform string
want string
}{
// No name/version, no platform.
{
want: fmt.Sprintf("NoClient/0.0.1 (%v)", runtime.GOOS),
},
// No name/version, with platform.
{
platform: "macOS 10.15",
want: "NoClient/0.0.1 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Mac OS X Mail",
version: "1.0.0",
platform: "macOS 10.15",
want: "Mac OS X Mail/1.0.0 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Mac OS X Mail",
version: "13.4 (3608.120.23.2.4)",
platform: "macOS 10.15",
want: "Mac OS X Mail/13.4-3608.120.23.2.4 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Thunderbird",
version: "78.6.1",
platform: "Windows 10 (10.0)",
want: "Thunderbird/78.6.1 (Windows 10 (10.0))",
},
}
for _, test := range tests {
test := test
t.Run(test.want, func(t *testing.T) {
ua := New()
if test.name != "" && test.version != "" {
ua.SetClient(test.name, test.version)
}
if test.platform != "" {
ua.SetPlatform(test.platform)
}
assert.Equal(t, test.want, ua.String())
})
}
}

View File

@ -1,41 +0,0 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package constants contains variables that are set via ldflags during build.
package constants
import "fmt"
const VendorName = "protonmail"
// nolint[gochecknoglobals]
var (
// Version of the build.
Version = ""
// Revision is current hash of the build.
Revision = ""
// BuildTime stamp of the build.
BuildTime = ""
// DSNSentry client keys to be able to report crashes to Sentry.
DSNSentry = ""
// BuildVersion is derived from LongVersion and BuildTime.
BuildVersion = fmt.Sprintf("%v (%v) %v", Version, Revision, BuildTime)
)

View File

@ -1,143 +0,0 @@
// 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
}

View File

@ -1,179 +0,0 @@
// 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)
}

View File

@ -1,42 +0,0 @@
// 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,
)
}
}

View File

@ -1,54 +0,0 @@
// 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/internal/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")
}
}
}

View File

@ -1,58 +0,0 @@
// 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")
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -27,7 +27,6 @@ import (
// Constants of events used by the event listener in bridge. // Constants of events used by the event listener in bridge.
const ( const (
ErrorEvent = "error" ErrorEvent = "error"
CredentialsErrorEvent = "credentialsError"
CloseConnectionEvent = "closeConnection" CloseConnectionEvent = "closeConnection"
LogoutEvent = "logout" LogoutEvent = "logout"
AddressChangedEvent = "addressChanged" AddressChangedEvent = "addressChanged"
@ -49,9 +48,6 @@ const (
// SetupEvents specific to event type and data. // SetupEvents specific to event type and data.
func SetupEvents(listener listener.Listener) { func SetupEvents(listener listener.Listener) {
listener.SetLimit(LogoutEvent, LogoutEventTimeout) listener.SetLimit(LogoutEvent, LogoutEventTimeout)
listener.SetBuffer(ErrorEvent)
listener.SetBuffer(CredentialsErrorEvent)
listener.SetBuffer(InternetOffEvent)
listener.SetBuffer(UpgradeApplicationEvent)
listener.SetBuffer(TLSCertIssue) listener.SetBuffer(TLSCertIssue)
listener.SetBuffer(ErrorEvent)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -30,7 +30,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/mobileconfig" mobileconfig "github.com/ProtonMail/go-apple-mobileconfig"
) )
func init() { //nolint[gochecknoinit] func init() { //nolint[gochecknoinit]
@ -43,7 +43,7 @@ func (c *appleMail) Name() string {
return "Apple Mail" return "Apple Mail"
} }
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) error { //nolint[funlen] func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.BridgeUser, addressIndex int) error { //nolint[funlen]
var addresses string var addresses string
var displayName string var displayName string
@ -66,17 +66,17 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
EmailAddress: addresses, EmailAddress: addresses,
DisplayName: displayName, DisplayName: displayName,
Identifier: "protonmail " + displayName + timestamp, Identifier: "protonmail " + displayName + timestamp,
IMAP: &mobileconfig.IMAP{ Imap: &mobileconfig.Imap{
Hostname: bridge.Host, Hostname: bridge.Host,
Port: imapPort, Port: imapPort,
TLS: imapSSL, Tls: imapSSL,
Username: displayName, Username: displayName,
Password: user.GetBridgePassword(), Password: user.GetBridgePassword(),
}, },
SMTP: &mobileconfig.SMTP{ Smtp: &mobileconfig.Smtp{
Hostname: bridge.Host, Hostname: bridge.Host,
Port: smtpPort, Port: smtpPort,
TLS: smtpSSL, Tls: smtpSSL,
Username: displayName, Username: displayName,
}, },
} }
@ -93,12 +93,12 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
})() })()
// Make sure the file is only readable for the current user. // Make sure the file is only readable for the current user.
f, err := os.OpenFile(filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig")), os.O_RDWR|os.O_CREATE, 0600) f, err := os.OpenFile(filepath.Join(dir, "protonmail.mobileconfig"), os.O_RDWR|os.O_CREATE, 0600)
if err != nil { if err != nil {
return err return err
} }
if err := mc.WriteOut(f); err != nil { if err := mc.WriteTo(f); err != nil {
_ = f.Close() _ = f.Close()
return err return err
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -23,7 +23,7 @@ import "github.com/ProtonMail/proton-bridge/internal/frontend/types"
type AutoConfig interface { type AutoConfig interface {
Name() string Name() string
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.User, addressIndex int) error Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.BridgeUser, addressIndex int) error
} }
var available []AutoConfig //nolint[gochecknoglobals] var available []AutoConfig //nolint[gochecknoglobals]

View File

@ -1,100 +0,0 @@
// 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
}

View File

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

View File

@ -1,236 +0,0 @@
// 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) {}

View File

@ -1,232 +0,0 @@
// 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.Username(), 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.Username(), 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.Username(), 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.Username(), 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(),
)
}
}

View File

@ -1,50 +0,0 @@
// 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/")
}

View File

@ -1,128 +0,0 @@
// 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.
`)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // 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.User { func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
user := f.getUserByIndexOrName("") user := f.getUserByIndexOrName("")
if user != nil { if user != nil {
return user return user
@ -76,7 +76,7 @@ func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.User {
return user return user
} }
func (f *frontendCLI) getUserByIndexOrName(arg string) types.User { func (f *frontendCLI) getUserByIndexOrName(arg string) types.BridgeUser {
users := f.bridge.GetUsers() users := f.bridge.GetUsers()
numberOfAccounts := len(users) numberOfAccounts := len(users)
if numberOfAccounts == 0 { if numberOfAccounts == 0 {

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -21,14 +21,14 @@ import (
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge" "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/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
) )
func (f *frontendCLI) listAccounts(c *ishell.Context) { func (f *frontendCLI) listAccounts(c *ishell.Context) {
spacing := "%-2d: %-20s (%-15s, %-15s)\n" spacing := "%-2d: %-20s (%-15s, %-15s)\n"
f.Printf(bold(strings.ReplaceAll(spacing, "d", "s")), "#", "account", "status", "address mode") f.Printf(bold(strings.Replace(spacing, "d", "s", -1)), "#", "account", "status", "address mode")
for idx, user := range f.bridge.GetUsers() { for idx, user := range f.bridge.GetUsers() {
connected := "disconnected" connected := "disconnected"
if user.IsConnected() { if user.IsConnected() {
@ -63,23 +63,23 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
} }
} }
func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) { func (f *frontendCLI) showAccountAddressInfo(user types.BridgeUser, address string) {
smtpSecurity := "STARTTLS" smtpSecurity := "STARTTLS"
if f.settings.GetBool(settings.SMTPSSLKey) { if f.preferences.GetBool(preferences.SMTPSSLKey) {
smtpSecurity = "SSL" smtpSecurity = "SSL"
} }
f.Println(bold("Configuration for " + address)) f.Println(bold("Configuration for " + address))
f.Printf("IMAP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n", f.Printf("IMAP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
bridge.Host, bridge.Host,
f.settings.GetInt(settings.IMAPPortKey), f.preferences.GetInt(preferences.IMAPPortKey),
address, address,
user.GetBridgePassword(), user.GetBridgePassword(),
"STARTTLS", "STARTTLS",
) )
f.Println("") f.Println("")
f.Printf("SMTP Settings\nAddress: %s\nSMTP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n", f.Printf("SMTP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
bridge.Host, bridge.Host,
f.settings.GetInt(settings.SMTPPortKey), f.preferences.GetInt(preferences.SMTPPortKey),
address, address,
user.GetBridgePassword(), user.GetBridgePassword(),
smtpSecurity, smtpSecurity,
@ -126,7 +126,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return return
} }
err = client.Auth2FA(twoFactor, auth) _, err = client.Auth2FA(twoFactor, auth)
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -19,54 +19,50 @@
package cli package cli
import ( import (
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
"github.com/sirupsen/logrus"
) )
var ( var (
log = logrus.WithField("pkg", "frontend/cli") //nolint[gochecknoglobals] log = config.GetLogEntry("frontend/cli") //nolint[gochecknoglobals]
) )
type frontendCLI struct { type frontendCLI struct {
*ishell.Shell *ishell.Shell
locations *locations.Locations config *config.Config
settings *settings.Settings preferences *config.Preferences
eventListener listener.Listener eventListener listener.Listener
updater types.Updater updates types.Updater
bridge types.Bridger bridge types.Bridger
restarter types.Restarter appRestart bool
} }
// New returns a new CLI frontend configured with the given options. // New returns a new CLI frontend configured with the given options.
func New( //nolint[funlen] func New( //nolint[funlen]
panicHandler types.PanicHandler, panicHandler types.PanicHandler,
config *config.Config,
locations *locations.Locations, preferences *config.Preferences,
settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updates types.Updater,
bridge types.Bridger, bridge types.Bridger,
restarter types.Restarter,
) *frontendCLI { //nolint[golint] ) *frontendCLI { //nolint[golint]
fe := &frontendCLI{ fe := &frontendCLI{
Shell: ishell.New(), Shell: ishell.New(),
locations: locations, config: config,
settings: settings, preferences: preferences,
eventListener: eventListener, eventListener: eventListener,
updater: updater, updates: updates,
bridge: bridge, bridge: bridge,
restarter: restarter, appRestart: false,
} }
// Clear commands. // Clear commands.
@ -80,7 +76,7 @@ func New( //nolint[funlen]
Func: fe.deleteCache, Func: fe.deleteCache,
}) })
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts", clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: a, k, keychain)", Help: "remove all accounts from keychain. (aliases: k, keychain)",
Aliases: []string{"a", "k", "keychain"}, Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts, Func: fe.deleteAccounts,
}) })
@ -102,6 +98,10 @@ func New( //nolint[funlen]
Aliases: []string{"p"}, Aliases: []string{"p"},
Func: fe.changePort, Func: fe.changePort,
}) })
changeCmd.AddCmd(&ishell.Cmd{Name: "proxy",
Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.toggleAllowProxy,
})
changeCmd.AddCmd(&ishell.Cmd{Name: "smtp-security", changeCmd.AddCmd(&ishell.Cmd{Name: "smtp-security",
Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)", Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)",
Aliases: []string{"ssl", "starttls"}, Aliases: []string{"ssl", "starttls"},
@ -109,56 +109,13 @@ func New( //nolint[funlen]
}) })
fe.AddCmd(changeCmd) fe.AddCmd(changeCmd)
// DoH commands.
dohCmd := &ishell.Cmd{Name: "proxy",
Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
}
dohCmd.AddCmd(&ishell.Cmd{Name: "allow",
Help: "allow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.allowProxy,
})
dohCmd.AddCmd(&ishell.Cmd{Name: "disallow",
Help: "disallow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.disallowProxy,
})
fe.AddCmd(dohCmd)
// Updates commands.
updatesCmd := &ishell.Cmd{Name: "updates",
Help: "manage bridge updates",
}
updatesCmd.AddCmd(&ishell.Cmd{Name: "check",
Help: "check for Bridge updates",
Func: fe.checkUpdates,
})
autoUpdatesCmd := &ishell.Cmd{Name: "autoupdates",
Help: "manage bridge updates",
}
updatesCmd.AddCmd(autoUpdatesCmd)
autoUpdatesCmd.AddCmd(&ishell.Cmd{Name: "enable",
Help: "automatically keep bridge up to date",
Func: fe.enableAutoUpdates,
})
autoUpdatesCmd.AddCmd(&ishell.Cmd{Name: "disable",
Help: "require bridge to be manually updated",
Func: fe.disableAutoUpdates,
})
updatesChannelCmd := &ishell.Cmd{Name: "channel",
Help: "switch updates channel",
}
updatesCmd.AddCmd(updatesChannelCmd)
updatesChannelCmd.AddCmd(&ishell.Cmd{Name: "early",
Help: "switch to the early-access updates channel",
Func: fe.selectEarlyChannel,
})
updatesChannelCmd.AddCmd(&ishell.Cmd{Name: "stable",
Help: "switch to the stable updates channel",
Func: fe.selectStableChannel,
})
fe.AddCmd(updatesCmd)
// Check commands. // Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."} checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
Help: "check for Bridge updates. (aliases: u, v, version)",
Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates,
})
checkCmd.AddCmd(&ishell.Cmd{Name: "internet", checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)", Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"}, Aliases: []string{"i", "con", "connection"},
@ -177,7 +134,11 @@ func New( //nolint[funlen]
Aliases: []string{"man"}, Aliases: []string{"man"},
Func: fe.printManual, 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", fe.AddCmd(&ishell.Cmd{Name: "credits",
Help: "print used resources.", Help: "print used resources.",
Func: fe.printCredits, Func: fe.printCredits,
@ -224,12 +185,13 @@ func New( //nolint[funlen]
defer panicHandler.HandlePanic() defer panicHandler.HandlePanic()
fe.watchEvents() fe.watchEvents()
}() }()
fe.eventListener.RetryEmit(events.TLSCertIssue)
fe.eventListener.RetryEmit(events.ErrorEvent)
return fe return fe
} }
func (f *frontendCLI) watchEvents() { func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent) errorCh := f.getEventChannel(events.ErrorEvent)
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent) internetOffCh := f.getEventChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent) internetOnCh := f.getEventChannel(events.InternetOnEvent)
addressChangedCh := f.getEventChannel(events.AddressChangedEvent) addressChangedCh := f.getEventChannel(events.AddressChangedEvent)
@ -240,8 +202,6 @@ func (f *frontendCLI) watchEvents() {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
f.Println("Bridge failed:", errorDetails) f.Println("Bridge failed:", errorDetails)
case <-credentialsErrorCh:
f.notifyCredentialsError()
case <-internetOffCh: case <-internetOffCh:
f.notifyInternetOff() f.notifyInternetOff()
case <-internetOnCh: case <-internetOnCh:
@ -265,12 +225,23 @@ func (f *frontendCLI) watchEvents() {
func (f *frontendCLI) getEventChannel(event string) <-chan string { func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string) ch := make(chan string)
f.eventListener.Add(event, ch) f.eventListener.Add(event, ch)
f.eventListener.RetryEmit(event)
return ch return ch
} }
// IsAppRestarting returns whether the app is currently set to restart.
func (f *frontendCLI) IsAppRestarting() bool {
return f.appRestart
}
// Loop starts the frontend loop with an interactive shell. // Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop() error { func (f *frontendCLI) Loop(credentialsError error) error {
if credentialsError != nil {
f.notifyCredentialsError()
return credentialsError
}
f.preferences.SetBool(preferences.FirstStartKey, false)
f.Print(` f.Print(`
Welcome to ProtonMail Bridge interactive shell Welcome to ProtonMail Bridge interactive shell
___....___ ___....___
@ -291,12 +262,3 @@ func (f *frontendCLI) Loop() error {
f.Run() f.Run()
return nil 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) {}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -22,7 +22,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/connection"
"github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
) )
@ -34,25 +36,21 @@ var (
func (f *frontendCLI) restart(c *ishell.Context) { func (f *frontendCLI) restart(c *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to restart the Bridge") { if f.yesNoQuestion("Are you sure you want to restart the Bridge") {
f.Println("Restarting Bridge...") f.Println("Restarting Bridge...")
f.restarter.SetToRestart() f.appRestart = true
f.Stop() f.Stop()
} }
} }
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) { func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.bridge.CheckConnection() == nil { if connection.CheckInternetConnection() == nil {
f.Println("Internet connection is available.") f.Println("Internet connection is available.")
} else { } else {
f.Println("Can not contact the server, please check your internet connection.") f.Println("Can not contact server please check you internet connection.")
} }
} }
func (f *frontendCLI) printLogDir(c *ishell.Context) { func (f *frontendCLI) printLogDir(c *ishell.Context) {
if path, err := f.locations.ProvideLogsPath(); err != nil { f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
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) { func (f *frontendCLI) printManual(c *ishell.Context) {
@ -73,7 +71,7 @@ func (f *frontendCLI) deleteCache(c *ishell.Context) {
f.Println("Cached cleared, restarting bridge") f.Println("Cached cleared, restarting bridge")
// Clearing data removes everything (db, preferences, ...) // Clearing data removes everything (db, preferences, ...)
// so everything has to be stopped and started again. // so everything has to be stopped and started again.
f.restarter.SetToRestart() f.appRestart = true
f.Stop() f.Stop()
} }
@ -81,7 +79,7 @@ func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
f.ShowPrompt(false) f.ShowPrompt(false)
defer f.ShowPrompt(true) defer f.ShowPrompt(true)
isSSL := f.settings.GetBool(settings.SMTPSSLKey) isSSL := f.preferences.GetBool(preferences.SMTPSSLKey)
newSecurity := "SSL" newSecurity := "SSL"
if isSSL { if isSSL {
newSecurity = "STARTTLS" newSecurity = "STARTTLS"
@ -90,9 +88,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) msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q and restart the Bridge", newSecurity)
if f.yesNoQuestion(msg) { if f.yesNoQuestion(msg) {
f.settings.SetBool(settings.SMTPSSLKey, !isSSL) f.preferences.SetBool(preferences.SMTPSSLKey, !isSSL)
f.Println("Restarting Bridge...") f.Println("Restarting Bridge...")
f.restarter.SetToRestart() f.appRestart = true
f.Stop() f.Stop()
} }
} }
@ -101,14 +99,14 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
f.ShowPrompt(false) f.ShowPrompt(false)
defer f.ShowPrompt(true) defer f.ShowPrompt(true)
currentPort = f.settings.Get(settings.IMAPPortKey) currentPort = f.preferences.Get(preferences.IMAPPortKey)
newIMAPPort := f.readStringInAttempts("Set IMAP port (current "+currentPort+")", c.ReadLine, f.isPortFree) newIMAPPort := f.readStringInAttempts("Set IMAP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
if newIMAPPort == "" { if newIMAPPort == "" {
newIMAPPort = currentPort newIMAPPort = currentPort
} }
imapPortChanged := newIMAPPort != currentPort imapPortChanged := newIMAPPort != currentPort
currentPort = f.settings.Get(settings.SMTPPortKey) currentPort = f.preferences.Get(preferences.SMTPPortKey)
newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree) newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
if newSMTPPort == "" { if newSMTPPort == "" {
newSMTPPort = currentPort newSMTPPort = currentPort
@ -122,46 +120,34 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
if imapPortChanged || smtpPortChanged { if imapPortChanged || smtpPortChanged {
f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort) f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort)
f.settings.Set(settings.IMAPPortKey, newIMAPPort) f.preferences.Set(preferences.IMAPPortKey, newIMAPPort)
f.settings.Set(settings.SMTPPortKey, newSMTPPort) f.preferences.Set(preferences.SMTPPortKey, newSMTPPort)
f.Println("Restarting Bridge...") f.Println("Restarting Bridge...")
f.restarter.SetToRestart() f.appRestart = true
f.Stop() f.Stop()
} else { } else {
f.Println("Nothing changed") f.Println("Nothing changed")
} }
} }
func (f *frontendCLI) allowProxy(c *ishell.Context) { func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
if f.settings.GetBool(settings.AllowProxyKey) { if f.preferences.GetBool(preferences.AllowProxyKey) {
f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.")
return
}
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.settings.SetBool(settings.AllowProxyKey, true)
f.bridge.AllowProxy()
}
}
func (f *frontendCLI) disallowProxy(c *ishell.Context) {
if !f.settings.GetBool(settings.AllowProxyKey) {
f.Println("Bridge is already set to NOT use alternative routing to connect to Proton if it is being blocked.")
return
}
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.") f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") { if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.settings.SetBool(settings.AllowProxyKey, false) f.preferences.SetBool(preferences.AllowProxyKey, false)
f.bridge.DisallowProxy() bridge.DisallowDoH()
}
} 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)
bridge.AllowDoH()
}
} }
} }
func (f *frontendCLI) isPortFree(port string) bool { func (f *frontendCLI) isPortFree(port string) bool {
port = strings.ReplaceAll(port, ":", "") port = strings.Replace(port, ":", "", -1)
if port == "" || port == currentPort { if port == "" || port == currentPort {
return true return true
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -21,22 +21,40 @@ import (
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
) )
func (f *frontendCLI) checkUpdates(c *ishell.Context) { func (f *frontendCLI) checkUpdates(c *ishell.Context) {
version, err := f.updater.Check() isUpToDate, latestVersionInfo, err := f.updates.CheckIsBridgeUpToDate()
if err != nil { if err != nil {
f.Println("An error occurred while checking for updates.") f.printAndLogError("Cannot retrieve version info: ", err)
f.checkInternetConnection(c)
return return
} }
if isUpToDate {
if f.updater.IsUpdateApplicable(version) {
f.Println("An update is available.")
} else {
f.Println("Your version is up to date.") 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)
} }
} }
@ -45,68 +63,3 @@ func (f *frontendCLI) printCredits(c *ishell.Context) {
f.Println(pkg) f.Println(pkg)
} }
} }
func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) {
if f.settings.GetBool(settings.AutoUpdateKey) {
f.Println("Bridge is already set to automatically install updates.")
return
}
f.Println("Bridge is currently set to NOT automatically install updates.")
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
f.settings.SetBool(settings.AutoUpdateKey, true)
}
}
func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) {
if !f.settings.GetBool(settings.AutoUpdateKey) {
f.Println("Bridge is already set to NOT automatically install updates.")
return
}
f.Println("Bridge is currently set to automatically install updates.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.settings.SetBool(settings.AutoUpdateKey, false)
}
}
func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) {
if f.bridge.GetUpdateChannel() == updater.EarlyChannel {
f.Println("Bridge is already on the early-access update channel.")
return
}
f.Println("Bridge is currently on the stable update channel.")
if f.yesNoQuestion("Are you sure you want to switch to the early-access update channel") {
needRestart, err := f.bridge.SetUpdateChannel(updater.EarlyChannel)
if err != nil {
f.Println("There was a problem switching update channel.")
}
if needRestart {
f.restarter.SetToRestart()
}
}
}
func (f *frontendCLI) selectStableChannel(c *ishell.Context) {
if f.bridge.GetUpdateChannel() == updater.StableChannel {
f.Println("Bridge is already on the stable update channel.")
return
}
f.Println("Bridge is currently on the early-access update channel.")
f.Println("Switching to the stable channel may reset all data!")
if f.yesNoQuestion("Are you sure you want to switch to the stable update channel") {
needRestart, err := f.bridge.SetUpdateChannel(updater.StableChannel)
if err != nil {
f.Println("There was a problem switching update channel.")
}
if needRestart {
f.restarter.SetToRestart()
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -93,15 +93,10 @@ func (f *frontendCLI) notifyLogout(address string) {
} }
func (f *frontendCLI) notifyNeedUpgrade() { func (f *frontendCLI) notifyNeedUpgrade() {
version, err := f.updater.Check() f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
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] func (f *frontendCLI) notifyCredentialsError() {
// Print in 80-column width. // Print in 80-column width.
f.Println("ProtonMail Bridge is not able to detect a supported password manager") 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") f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -19,187 +19,69 @@
package frontend package frontend
import ( import (
"github.com/ProtonMail/go-autostart" "github.com/0xAX/notificator"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli" "github.com/ProtonMail/proton-bridge/internal/frontend/cli"
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt" "github.com/ProtonMail/proton-bridge/internal/frontend/qt"
qtie "github.com/ProtonMail/proton-bridge/internal/frontend/qt-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/importexport" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
) )
var ( var (
log = logrus.WithField("pkg", "frontend") // nolint[unused] log = config.GetLogEntry("frontend") // nolint[unused]
) )
// Frontend is an interface to be implemented by each frontend type (cli, gui, html). // Frontend is an interface to be implemented by each frontend type (cli, gui, html).
type Frontend interface { type Frontend interface {
Loop() error Loop(credentialsError error) error
NotifyManualUpdate(update updater.VersionInfo, canInstall bool) IsAppRestarting() bool
SetVersion(update updater.VersionInfo) }
NotifySilentUpdateInstalled()
NotifySilentUpdateError(error) // HandlePanic handles panics which occur for users with GUI.
WaitUntilFrontendIsReady() 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)
} }
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`. // New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
func New( func New(
version, version,
buildVersion, buildVersion,
programName,
frontendType string, frontendType string,
showWindowOnStart bool, showWindowOnStart bool,
panicHandler types.PanicHandler, panicHandler types.PanicHandler,
locations *locations.Locations, config *config.Config,
settings *settings.Settings, preferences *config.Preferences,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updates types.Updater,
userAgent *useragent.UserAgent,
bridge *bridge.Bridge, bridge *bridge.Bridge,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App,
restarter types.Restarter,
) Frontend { ) Frontend {
bridgeWrap := types.NewBridgeWrap(bridge) bridgeWrap := types.NewBridgeWrap(bridge)
return newBridgeFrontend( return new(version, buildVersion, frontendType, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridgeWrap, noEncConfirmator)
version,
buildVersion,
programName,
frontendType,
showWindowOnStart,
panicHandler,
locations,
settings,
eventListener,
updater,
userAgent,
bridgeWrap,
noEncConfirmator,
autostart,
restarter,
)
} }
func newBridgeFrontend( func new(
version, version,
buildVersion, buildVersion,
programName,
frontendType string, frontendType string,
showWindowOnStart bool, showWindowOnStart bool,
panicHandler types.PanicHandler, panicHandler types.PanicHandler,
locations *locations.Locations, config *config.Config,
settings *settings.Settings, preferences *config.Preferences,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updates types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger, bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App,
restarter types.Restarter,
) Frontend { ) Frontend {
switch frontendType { switch frontendType {
case "cli": case "cli":
return cli.New( return cli.New(panicHandler, config, preferences, eventListener, updates, bridge)
panicHandler,
locations,
settings,
eventListener,
updater,
bridge,
restarter,
)
default: default:
return qt.New( return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
version,
buildVersion,
programName,
showWindowOnStart,
panicHandler,
locations,
settings,
eventListener,
updater,
userAgent,
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,
)
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,194 +0,0 @@
// 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/>.
// Change default keychain dialog
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
import QtQuick.Controls 2.2 as QC
import QtQuick.Layouts 1.0
Dialog {
id: root
title : "Change which keychain Bridge uses as default"
subtitle : "Select which keychain is used (Bridge will automatically restart)"
isDialogBusy: currentIndex==1
property var selectedKeychain
Connections {
target: go.selectedKeychain
onValueChanged: {
console.debug("go.selectedKeychain == ", go.selectedKeychain)
}
}
ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.minimumHeight: root.titleHeight + Style.dialog.heightSeparator
Layout.maximumHeight: root.titleHeight + Style.dialog.heightSeparator
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ColumnLayout {
anchors.centerIn: parent
Repeater {
id: keychainRadioButtons
model: go.availableKeychain
QC.RadioButton {
id: radioDelegate
text: modelData
checked: go.selectedKeychain === modelData
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: Style.main.spacing
indicator: Text {
text : radioDelegate.checked ? Style.fa.check_circle : Style.fa.circle_o
color : radioDelegate.checked ? Style.main.textBlue : Style.main.textInactive
font {
pointSize: Style.dialog.iconSize * Style.pt
family: Style.fontawesome.name
}
}
contentItem: Text {
text: radioDelegate.text
color: Style.main.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: checked
}
horizontalAlignment : Text.AlignHCenter
verticalAlignment : Text.AlignVCenter
leftPadding: Style.dialog.iconSize
}
onCheckedChanged: {
if (checked) {
root.selectedKeychain = modelData
}
}
}
}
Item {
Layout.fillWidth: true
Layout.minimumHeight: Style.dialog.heightSeparator
Layout.maximumHeight: Style.dialog.heightSeparator
}
Row {
id: buttonRow
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main: Style.dialog.text
fa_icon: Style.fa.times
text: qsTr("Cancel", "dismisses current action")
onClicked : root.hide()
}
ButtonRounded {
id: buttonYes
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
fa_icon: Style.fa.check
text: qsTr("Okay", "confirms and dismisses a notification")
onClicked : root.confirmed()
}
}
}
}
}
ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.minimumHeight: root.titleHeight + Style.dialog.heightSeparator
Layout.maximumHeight: root.titleHeight + Style.dialog.heightSeparator
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Text {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width : parent.width/2
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
text : "Default keychain is now set to " + root.selectedKeychain +
"\n\n" +
qsTr("Settings will be applied after the next start.", "notification about setting being applied after next start") +
"\n\n" +
qsTr("Bridge will now restart.", "notification about restarting")
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
}
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
function confirmed() {
if (selectedKeychain === go.selectedKeychain) {
root.hide()
return
}
incrementCurrentIndex()
timer.start()
}
timer.interval : 5000
Connections {
target: timer
onTriggered: {
// This action triggers restart on the backend side.
go.selectedKeychain = selectedKeychain
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -226,7 +226,7 @@ Dialog {
target: timer target: timer
onTriggered: { onTriggered: {
go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked) go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked)
go.setToRestart() go.isRestarting = true
Qt.quit() Qt.quit()
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -137,7 +137,6 @@ Dialog {
spacing: Style.dialog.spacing spacing: Style.dialog.spacing
ButtonRounded { ButtonRounded {
id:buttonNo id:buttonNo
visible: root.state != "toggleEarlyAccess"
color_main: Style.dialog.text color_main: Style.dialog.text
fa_icon: Style.fa.times fa_icon: Style.fa.times
text: qsTr("No") text: qsTr("No")
@ -149,7 +148,7 @@ Dialog {
color_minor: Style.main.textBlue color_minor: Style.main.textBlue
isOpaque: true isOpaque: true
fa_icon: Style.fa.check fa_icon: Style.fa.check
text: root.state == "toggleEarlyAccess" ? qsTr("Ok") : qsTr("Yes") text: qsTr("Yes")
onClicked : { onClicked : {
currentIndex=1 currentIndex=1
root.confirmed() root.confirmed()
@ -229,7 +228,7 @@ Dialog {
currentIndex : 0 currentIndex : 0
title : qsTr("Clear cache", "title of page that displays during cache clearing") 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") 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 as well as cached email data for all accounts, and requires you to reconfigure your client.", "displays during cache clearing") 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.", "displays during cache clearing")
answer : qsTr("Clearing the cache ...", "displays during cache clearing") answer : qsTr("Clearing the cache ...", "displays during cache clearing")
} }
}, },
@ -293,28 +292,6 @@ 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, and requires you to reconfigure your client.")
title : qsTr("Disable early access")
answer : qsTr("Disabling early access...")
}
},
State { State {
name: "noKeychain" name: "noKeychain"
PropertyChanges { PropertyChanges {
@ -363,9 +340,13 @@ Dialog {
winMain.dialogAddUser .visible = false winMain.dialogAddUser .visible = false
winMain.dialogChangePort .visible = false winMain.dialogChangePort .visible = false
winMain.dialogCredits .visible = false winMain.dialogCredits .visible = false
winMain.dialogVersionInfo .visible = false
// dialogFirstStart should reappear again after closing global
root.visible = true root.visible = true
} }
onConfirmed : { onConfirmed : {
if (state == "quit" || state == "instance exists" ) { if (state == "quit" || state == "instance exists" ) {
timer.interval = 1000 timer.interval = 1000
@ -386,12 +367,10 @@ Dialog {
if ( state == "logout" ) { go.logoutAccount (input) } if ( state == "logout" ) { go.logoutAccount (input) }
if ( state == "toggleAutoStart" ) { go.toggleAutoStart () } if ( state == "toggleAutoStart" ) { go.toggleAutoStart () }
if ( state == "toggleAllowProxy" ) { go.toggleAllowProxy () } if ( state == "toggleAllowProxy" ) { go.toggleAllowProxy () }
if ( state == "toggleEarlyAccessOn" ) { go.toggleEarlyAccess () }
if ( state == "toggleEarlyAccessOff" ) { go.toggleEarlyAccess () }
if ( state == "quit" ) { Qt.quit () } if ( state == "quit" ) { Qt.quit () }
if ( state == "instance exists" ) { Qt.quit () } if ( state == "instance exists" ) { Qt.quit () }
if ( state == "noKeychain" ) { Qt.quit () } if ( state == "noKeychain" ) { Qt.quit () }
if ( state == "checkUpdates" ) { } if ( state == "checkUpdates" ) { go.runCheckVersion (true) }
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -74,7 +74,9 @@ Item {
rightIcon.text : Style.fa.chevron_circle_right rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: { onClicked: {
go.checkForUpdates() dialogGlobal.state="checkUpdates"
dialogGlobal.show()
dialogGlobal.confirmed()
} }
} }
@ -100,7 +102,7 @@ Item {
Row { Row {
anchors.left : parent.left anchors.left : parent.left
Rectangle { height: Style.dialog.spacing; width: (wrapper.width - credits.width - licenseFile.width - release.width - sepaCreditsRelease.width)/2; color: "transparent"} Rectangle { height: Style.dialog.spacing; width: (wrapper.width- credits.width - release.width - sepaCreditsRelease.width)/2; color: "transparent"}
ClickIconText { ClickIconText {
id:credits id:credits
@ -112,20 +114,6 @@ Item {
onClicked : winMain.dialogCredits.show() 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"} Rectangle {id: sepaCreditsRelease ; height: Style.dialog.spacing; width: Style.main.dummy; color: "transparent"}
ClickIconText { ClickIconText {
@ -135,7 +123,10 @@ Item {
textColor : Style.main.textDisabled textColor : Style.main.textDisabled
fontSize : Style.main.fontSize fontSize : Style.main.fontSize
textUnderline : true textUnderline : true
onClicked : gui.openReleaseNotes() onClicked : {
go.getLocalVersionInfo()
winMain.dialogVersionInfo.show()
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -38,6 +38,7 @@ Window {
property alias dialogUpdate : dialogUpdate property alias dialogUpdate : dialogUpdate
property alias dialogFirstStart : dialogFirstStart property alias dialogFirstStart : dialogFirstStart
property alias dialogGlobal : dialogGlobal property alias dialogGlobal : dialogGlobal
property alias dialogVersionInfo : dialogVersionInfo
property alias dialogConnectionTroubleshoot : dialogConnectionTroubleshoot property alias dialogConnectionTroubleshoot : dialogConnectionTroubleshoot
property alias bubbleNote : bubbleNote property alias bubbleNote : bubbleNote
property alias addAccountTip : addAccountTip property alias addAccountTip : addAccountTip
@ -65,6 +66,7 @@ Window {
!dialogUpdate .visible && !dialogUpdate .visible &&
!dialogFirstStart .visible && !dialogFirstStart .visible &&
!dialogGlobal .visible && !dialogGlobal .visible &&
!dialogVersionInfo .visible &&
!bubbleNote .visible !bubbleNote .visible
Accessible.role: Accessible.Grouping Accessible.role: Accessible.Grouping
@ -303,10 +305,6 @@ Window {
id: dialogChangePort id: dialogChangePort
} }
DialogKeychainChange {
id: dialogChangeKeychain
}
DialogConnectionTroubleshoot { DialogConnectionTroubleshoot {
id: dialogConnectionTroubleshoot id: dialogConnectionTroubleshoot
} }
@ -318,7 +316,50 @@ Window {
DialogUpdate { DialogUpdate {
id: dialogUpdate id: dialogUpdate
forceUpdate: root.isOutdateVersion
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)
}
}
}
} }
@ -332,6 +373,25 @@ Window {
id: dialogTlsCert 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 { DialogYesNo {
id: dialogGlobal id: dialogGlobal
question : "" question : ""

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -84,7 +84,7 @@ Window {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
ButtonRounded { ButtonRounded {
id: sendAnyway id: cancel
onClicked : root.hide(true) onClicked : root.hide(true)
height: Style.main.fontSize*2 height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3 //width: Style.dialog.widthButton*1.3
@ -93,7 +93,7 @@ Window {
} }
ButtonRounded { ButtonRounded {
id: cancel id: sendAnyway
onClicked : root.hide(false) onClicked : root.hide(false)
height: Style.main.fontSize*2 height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3 //width: Style.dialog.widthButton*1.3

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //
@ -36,24 +36,6 @@ Item {
color: Style.main.background 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 // content
Column { Column {
anchors.left : parent.left anchors.left : parent.left
@ -66,7 +48,6 @@ Item {
text : qsTr("Clear", "clickable link next to clear cache button in settings") text : qsTr("Clear", "clickable link next to clear cache button in settings")
color: Style.main.text color: Style.main.text
font { font {
family : cacheClear.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt pointSize : Style.settings.fontSize * Style.pt
underline : true underline : true
} }
@ -85,7 +66,6 @@ Item {
text : qsTr("Clear", "clickable link next to clear keychain button in settings") text : qsTr("Clear", "clickable link next to clear keychain button in settings")
color: Style.main.text color: Style.main.text
font { font {
family : cacheKeychain.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt pointSize : Style.settings.fontSize * Style.pt
underline : true underline : true
} }
@ -115,50 +95,6 @@ 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 { ButtonIconText {
id: advancedSettings id: advancedSettings
property bool isAdvanced : !go.isDefaultPort property bool isAdvanced : !go.isDefaultPort
@ -189,7 +125,6 @@ Item {
text : qsTr("Change", "clickable link next to change ports button in settings") text : qsTr("Change", "clickable link next to change ports button in settings")
color: Style.main.text color: Style.main.text
font { font {
family : changePort.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt pointSize : Style.settings.fontSize * Style.pt
underline : true underline : true
} }
@ -240,24 +175,6 @@ Item {
} }
} }
ButtonIconText {
id: changeKeychain
visible: advancedSettings.isAdvanced && (go.availableKeychain.length > 1)
text: qsTr("Change keychain", "button to open dialog with default keychain selection")
leftIcon.text : Style.fa.key
rightIcon {
text : qsTr("Change", "clickable link next to change keychain button in settings")
color: Style.main.text
font {
family : changeKeychain.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogChangeKeychain.show()
}
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Proton Technologies AG // Copyright (c) 2020 Proton Technologies AG
// //
// This file is part of ProtonMail Bridge. // This file is part of ProtonMail Bridge.
// //

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