We build too many walls and not enough bridges

This commit is contained in:
Jakub
2020-04-08 12:59:16 +02:00
commit 17f4d6097a
494 changed files with 62753 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
Changelog.md merge=union

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# System files
*.app
*.DS_Store
# Editor files
.*.sw?
*~
# Compiled Object files, Static and Dynamic libs (Shared Objects)
vendor
# Test files
godog.test
debug.test
coverage.html
# Run files
mem.pprof
# Auto generated frontend
frontend/qml/BridgeUI/*.qmlc
frontend/qml/ProtonUI/*.qmlc
frontend/qml/ProtonUI/fontawesome.ttf
frontend/qml/ProtonUI/images
frontend/qml/*.qmlc

109
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,109 @@
image: gitlab.protontech.ch:4567/protonmail/desktop-bridge/ci
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- mkdir -p .cache/bin
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- make install-dev-dependencies
cache:
key: go-mod
paths:
- .cache
policy: pull
stages:
- image
- cache
- test
- build
# 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/protonmail/desktop-bridge/ci:latest ci
- docker push gitlab.protontech.ch:4567/protonmail/desktop-bridge/ci:latest
# Stage: CACHE
# This will ensure latest dependency versions and updates the cache for
# all other following jobs which only pull the cache.
cache-push:
stage: cache
only:
- branches
script:
- echo ""
cache:
key: go-mod
paths:
- .cache
# Stage: TEST
lint:
stage: test
only:
- branches
script:
- make lint
test:
stage: test
only:
- branches
script:
- apt-get -y install pass gnupg rng-tools
# First have enough of entropy (cat /proc/sys/kernel/random/entropy_avail).
- rngd -r /dev/urandom
# Generate GPG key without password for the password manager.
- gpg --batch --yes --passphrase '' --quick-generate-key 'tester@example.com'
# Use the last created GPG ID for the password manager.
- pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'`
# Then finally run the tests
- make test
test-integration:
stage: test
only:
- branches
script:
- VERBOSITY=debug make -C test test
dependency-updates:
stage: test
script:
- make updates
# Stage: BUILD
build-linux:
stage: build
# Test build every time (= we want to know build is possible).
only:
- branches
script:
- make build
artifacts:
name: "bridge-linux-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
expire_in: 2 week

0
.gitmodules vendored Normal file
View File

66
.golangci.yml Normal file
View File

@ -0,0 +1,66 @@
run:
timeout: 10m
build-tags:
- nogui
skip-dirs:
- pkg/mime
issues:
exclude-use-default: false
exclude:
- Using the variable on range scope `tt` in function literal
- should have comment (\([^)]+\) )?or be unexported # For now we are missing a lot of comments.
- at least one file in a package should have a package comment # For now we are missing a lot of comments.
exclude-rules:
- path: _test\.go
linters:
- dupl
- funlen
- gochecknoglobals
- gochecknoinits
- gosec
linters:
# setting disable-all will make only explicitly enabled linters run
disable-all: true
enable:
- deadcode # Finds unused code [fast: true, auto-fix: false]
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
- structcheck # Finds unused struct fields [fast: true, auto-fix: false]
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
- varcheck # Finds unused global variables and constants [fast: true, auto-fix: false]
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
- dupl # Tool for code clone detection [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]
- 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]
- 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]
- godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false]
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [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]
- 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]
- 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]
- 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]
- unconvert # Remove unnecessary type conversions [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]
#- wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false]
#- lll # Reports long lines [fast: true, auto-fix: false]

38
BUILDS.md Normal file
View File

@ -0,0 +1,38 @@
# Building ProtonMail Bridge app
## Prerequisites
* Go 1.13
* Bash with basic build utils: make, gcc, sed, find, grep, …
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (linux, windows) or Xcode (macOS)
* Windres (windows)
To enable the sending of crash reports using Sentry please set the
`main.DSNSentry` value with client key of your sentry project before build.
Otherwise sending of crash reports will be disabled.
## Build
* for Windows please unset the `MSYSTEM` variable
```bash
export MSYSTEM=
```
* in project root run
```bash
make build
```
* The result will be stored in `./cmd/Destop-Bridge/deploy/${GOOS}/`
* for `linux` binary will the name of project directory e.g `bridge`
* for `windows` the binary has extension `.exe` e.g `bridge.exe`
* for `darwin` the application will be created with name of project directory e.g `bridge.app`
## Usefull tests, lints and checks
In order to be able to run following commands please install development dependencies: `make install-dev-dependencies`
* `make test` will run unit test for whole project
* `make lint` will run liter for whole project
* `make -C ./tests test` will run integration tests for Bridge application
* `make run` will compile without GUI and start Bridge application in CLI mode

11
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,11 @@
# Contribution Policy
By making a contribution to this project:
1. I assign any and all copyright related to the contribution to
Proton Technologies AG;
2. I certify that the contribution was created in whole by me;
3. I understand and agree that this project and the contribution are public
and that a record of the contribution (including all personal information I
submit with it) is maintained indefinitely and may be redistributed with
this project or the open source license(s) involved.

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)

788
Changelog.md Normal file
View File

@ -0,0 +1,788 @@
# ProtonMail Bridge Changelog
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Unpublished
### Changed
* Adding DSN Sentry as build time parameter
## [v1.2.6] Donghai - beta (2020-03-XXX)
### Added
* GODT-145 support drafts
* GODT-211,GODT-231 fix updating subject and other fields
* GODT-220 fix deleting drafts
* GODT-224 fix creating draft from outlook without sender
* GODT-230,GODT-232 fix constructing sender address for drafts
* sync already synced draft to newly created drafts mailbox
* Add Subject to EventMessageUpdated in pmapi
* GODT-37 Add body and TLS handshake timeouts
* GODT-90 implement DOH (DNS over HTTPS) proxy
* Noninteractive mode
### Changed
* bump version go-1.14
* Bump dependencies:
| Repo | Old Version | New Version |
| github.com/0xAX/notificator | v0.0.0-20161214074916-82e921414e03 | v0.0.0-20191016112426-3962a5ea8da1 |
| github.com/ProtonMail/go-autostart | v0.0.0-20171017232241-85d98b097aae | v0.0.0-20181114175602-c5272053443a |
| github.com/abiosoft/ishell | v0.0.0-20171224170712-50251d04cb42 | v2.0.0+incompatible |
| github.com/emersion/go-sasl | v0.0.0-20161116183048-7e096a0a6197 | v0.0.0-20191210011802-430746ea8b9b |
| github.com/fatih/color | v1.7.0 | v1.9.0 |
| github.com/golang/mock | v1.4.2 | v1.4.3 |
| github.com/google/go-cmp | v0.3.1 | v0.4.0 |
| github.com/jaytaylor/html2text | v0.0.0-20190408195923-01ec452cbe43 | v0.0.0-20200220170450-61d9dc4d7195 |
| github.com/jhillyerd/enmime | v0.7.0 | v0.8.0 |
| github.com/logrusorgru/aurora | v0.0.0-20190803045625-94edacc10f9b | v0.0.0-20200102142835-e9ef32dff381 |
| github.com/skratchdot/open-golang | v0.0.0-20160302144031-75fb7ed4208c | v0.0.0-20200116055534-eef842397966 |
| github.com/stretchr/testify | v1.4.0 | v1.5.1 |
| github.com/therecipe/qt | v0.0.0-20191022233421-590f404884c9 | v0.0.0-20200126204426-5074eb6d8c41 |
| github.com/urfave/cli | v1.19.1 | v1.22.3 |
* pkg/updates: closing File reader to avoid too many opened files during update
* Created monorepo with bridge, pmapi, bridge utils, mime and srp
* One lint config for all packages and lint fixes in the code
* Fix tests for bridge utils to work on MacOS
* All tests use testify framework
* Processed TODOs or created issues
* Cleanup up comments
* GODT-169 reduce the number of keyring unlocks
* CSB-40 return error instead of panic in credential store
* #577 Avoid multiple send
* GODT-39 Sync is paging per message ID with ability to continue after interrupted sync
* Panic handler used in store for event loop and sync
* GODT-109 merge only 50 events into one
* Use v1.0.16 of pmapi
* GODT-236 requests to /messages/{read,unread,delete,undelete,label,unlabel} are paged with up to 100 message IDs
### Fixed
* GODT-227 Mitigate potential crash due to using a logged out pmapi client (proper fix to come in emma release)
* UserIDs were not checked when importing to Sent folder (affects copying from account1/sent to account2/sent)
## [v1.2.5] Charles - live (2020-03-11) beta (from 2020-02-10)
### Hotfix
* CSB-40 panic in credential store
* keyring unlocking locker
* no panic on failed html parse
* too many open files
### Added
* GODT-112 migration of preferences from c10 to c11
* GODT-100 test for external internal ID when appending to Sent folder to return APPEND UID otherwise skip with no error
* GODT-43 connection troubleshooting modal
* GODT-55 uid support in fake api
* GODT-88 increase uid validity on switch mode
* #951 Implementation of IMAP extension UIDPLUS
* #964 New store package, see Changed section
### Removed
* MOVE IMAP extension due to missing interaction with UIDPLUS
### Changed
* GODT-88 run mbox sync in parallel when switch password mode (re-init not user)
* GODT-95 do not throw error when trying to create new mailbox in IMAP root
* GODT-75 do not fail on unlabel inside delete
* #1095 always delete IMAP USER including wrong pasword
* unique pmapi client userID (including #1098)
* using go.enmime@v0.6.1 snapshot
* better detection of non-auth-error
* reset `hasAuthChannel` during logout for proper login functionality (set up auth channel and unlock keys)
* allow `APPEND` messages without parsable email address in sender field
* #1060 avoid `Append` after internal message ID was found and message was copyed to mailbox using `MessageLabel`
* #1049 Basic usage of store in SMTP package to poll event loop during sending message
* #1050 pollNow waits for events to be processed
* #1047 Fix fetch of empty mailbox
* #1046 Fix removing mailbox counts
* #1048 For any message build error return custom message
* When event loop exits with error it logs out user from Bridge
* #953 #984 First label messages before unlabeling when moving messages
* fixes after refactor:
* Slight memory optimization
* #1043 do not stuck in loop for updating message which does not exist anywhere anymore
* #1034 fix UID dynamic range for empty list
* fix of sequence number in IMAP IDLE expunge during deleting messages
* #1030 #1028 Fix some crashes and bad auths
* #953 #984 label messages first during moving them
* #964 (and #769,#899,#918,#930,#931,#949) refactor of IMAP
- Fix of sequence number in IMAP IDLE expunge during deleting messages
- Fix not-returning empty result for UID dynamic range as said in RFC
- Separated IMAP to store and IMAP
- Store is responsible for everything about db and calls to pmapi, including event loop, sync, address mode
- IMAP is responsible only for IMAP interfaces
- Event loop is only one per ProtonMail account (instead of one per alias)
- It also means only one database per account (instead of one per address)
- Changing address mode is not destroying database, only buckets with IDs mapping (keeping metadata for account)
- Before first sync we set event ID so we will not miss changes happening during sync
- Thanks to previous point we are not starting new sync when we finish first one because of unprocessed events
- Sync is not blocking event loop (user can get new messages even during sync)
- Sync is not blocking reading operations (user can list mailboxes even before first sync is done)
- Sync is not blocking writing operations such as mark messages read/unread and so on
- Most operations have to be passed to API and only event loop is writing them to the database
- Avoid relying on counts API endpoint; use event counts as much as possible
- Separate function for storing message content type and header into database
- Sequence number optimised for last item in mailbox
- Allow sending IMAP idle update to timeout to avoid blocking bridge
- Synchronisation will create a label if not yet present
- Labels and Folders (including system folders) are stored in DB together with their counts for offline read-out
- AddressIDs for all user addresses are stored in DB
- IMAP updates channel is set when an IMAP client connects (and IMAP updates are dropped until then)
- DB keeps track of address mode (split/combined)
* Event loop starts as soon as user is initialised (i.e. logged in), not just when imap is connected
* Use pmapi v1.0.13
* Logout user if initialisation fails
* Send UserRefreshEvent on user login and logout
* Use godog v0.8.0 under new name 'cucumber' (instead of DATA-DOG)
### Fixed
* #1057 Logging in to an already logged in user would display unrelated error "invalid mailbox password"
* #1056 Changing mailbox password sometimes didn't log out user
* #1066 Split address mode can not work when credentials store is cleared
* #1071 Bridge can think it is in combined mode when actually it's in split mode
* Missing `enmime` dependency
* Issue where a failed sync was not attempted again
* Removing an address would crash bridge
* #1087 Accounts with capital letters could not be added
* #1087 Inactive addresses were not filtered out of the store
* #1087 Unlock with correct key if message is sent to alias and not primary (aka original) address
* #1109 Receiving an event referencing an address that isn't present could crash bridge
* Avoid concurrent map writes in imap backend
* GODT-103 User keys were not unlocked later if they were not unlocked during startup
## [v1.2.4] Brooklyn beta (2019-12-16)
### Added
* #976: fix slow authentication
* Server security setting in info (GUI, CLI)
* default SSL for SMTP based on Mac version
* GUI/CLI items to controls SMTP security setup
* set new security and restart
### Changed
* #961 Timeouts for go-pmapi client with http.Transport
* Event poll with no change will hang forever. Using separate goroutine and timeout instead of proper fix (will be in refactor)
* Fixed an issue where entering an in-use port multiple times via the CLI would make bridge use it.
* Update therecipe/qt and Qt to 5.13
## [v1.2.3] Akashi - live (2019-11-05) beta (2019-10-22)
### Added
* #963 report first-start metric with bridge version
* #941 report new-login metric, report daily heartbeat
* #921 remote key lookup via Web Key Directory (WKD)
* #919 TLS issue notification in CLI
### Changed
* #769 #930 #931 #949 Syncing messages and fetching message and attachments in parallel with five workers
* #956 #741 update keychain
* Re-download and re-unlock user keyring when addresses are changed
* #944 Ugrade go-pmapi dependency to v1.0.4 to support phase one of the key migration
* #683 Password rehides each time password entry screen is shown
* Import-Export#219 fix double parameter definition
* Upgrade go-pm-bridge-utils dependency to v1.0.1
* #935 Fix wrong download link for linux updates.
* #952 fix error when sending mail with only BCC recipients (use empty slice instead of nil slice)
* Refactor `generateSendingInfo` to simplify logic; add test for this method.
* Generate code-coverage report with `make code-coverage`
* #942 fix focus window with logout message when trying to connect from the client
* Do not use panic for second instance
* #928 do not hide 'no keychain' dialog when upgrade is needed
* sending `NO` for errors while `FETCH`
* #899 Upgrade from Bolt to BBolt
* Upgrade to gopenpgp
* Bridge utils in own repository
* Code made compatible with name changes in go-pmapi
## [v1.2.2] - beta and live 2019-09-06
### Changed
* User compare case insensitive
## [v1.2.1] - beta and live 2019-09-05
### Changed
* #924 fix start of bridge without internet connection
## [v1.2.0] - beta 2019-08-22
### Added
* #903 added http.Client timeout to not hang out forever
* closing body after checking internet connection
* pedantic lint for bridgeUtils
* selected events are buffered and emited again when frontend loop is ready
* #890 implemented 2FA endpoint (auth split)
* #888 TLS Cert
* error bar and modal with explanation in GUI
* signal to show error
* add pinning to bridge (only for live API builds)
* #887 #883:
* wait before clearing data
* configer which provides pmapi.ClientConfig and app directories
* #861 restart after clear data
* panic handler for all goroutines
* CD for linux
* #798
* check counts after sync
* update counts in all mailboxes after sync
* `db.Mailbox.RemoveMissing`, `db.Mailbox.PutMany`
* `util.NotImplemented`
* tests for sync
* bridge core tests:
* introduced interfaces: `pmapiClienterFactory`, `pmapiClienter`, `credentialStorer`
* automatic mock generation for `listener.Listener`, `bridge.pmapiClienter`, `bridge.credentialStorer`
* #818 REFACTOR: see doc/code-structure.md
* Tests for bridge core & utils
* update user before `GetQuota`
* http bridge API
* bridge core tests:
* introduced interfaces: `pmapiClienterFactory`, `pmapiClienter`, `credentialStorer`
* automatic mock generation for `listener.Listener`, `bridge.pmapiClienter`, `bridge.credentialStorer`
* #774 start initialization with sync immediately after login
### Removed
* using `PutMeta` for DB to not rewrite header and size
* `Timeout` for connection (keep only `DialTimeout`)
* #798 `imapMailbox.sync`
* #818 REFACTOR: see doc/code-structure.md
* bridge global functions `GetAuth`, `GetAuthInfo`, `GetUserSettings` (using member functions of `pmapi.Client` instead)
* `backend.setCache`: not used
* IMAP extension for `XSTOP` and `XFOCUS`
* keychain `Disconnected` is not used, deleting directly (not using hide)
* `apiIdFrom(uid bool, id uint32)`, `apiIdRangeFromSeq(uid bool, seq imap.Seq)`: not used
* `server/dial.go` not used
* util `CustomMessage`, `StartTicker` not used
### Changed
* check before first even sync
* do sync in parallel from events
* closing event loop by CloseConnectionEvent
* allow client to log in with address only
* fix IMAP users lock
* #646 download headers when needed for first time
* #895 fix of parsing address list
* #844 do not spam GUI with logout events & sleep after bad login attempt from the client
* #887 #883 #898 #902 logout account from API and all IMAP connections before clearing cache for account
* #882 unassign PMAPI client after logout and force to run garbage collector
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail
* #838 `Sirupsen` -> `sirupsen`
* #893 save panic report file everytime
* #880 fix of informing user about outgoing non-encrypted e-mail
* fix aliases in split mode
* fix decrypted data in log notification
* #471 fix of using font awesome in regular text
* `SearchMessage` all IDs from DB not depends on `totalOnAPI`
* #798 populate efficiently
* improved `imap.db.mailbox.Counts()`
* `mbox.total,unread` -> `mbox.totalOnAPI,unreadOnAPI`
* always show DB status (even for `IDLE` updates)
* `imapUser.sync` now takes `labelID` as parameter
* split population by 1000 messages
* `db.User.put(msgs,keepCache)` is used in sync to not overwrite `msg.Size` and `msg.Header` in DB
* separate sync function from `backend.labelMailbox` class
* `UidNext` uses bolt sequence value instead of cursor position
* `util.tests.go` moved to `bridgeUtils`
* #471 fix of using font awesome in regular text
* #818 REFACTOR: see doc/code-structure.md
* No global states/variables anymore
* Code separated from one big package into smaller packages (bridge core, utils, IMAP, SMTP, API)
* Bridge core completely refactored - core should be API over credentials store and PMAPI
* Configuration and preferences are on one place; passed as dependency to all packages
* Bridge utils separated from the rest of the bridge code to be used in Import/Export
* Many channels converted into one listener which can register listeners and emit events to them
* Each package is ready to be used with interfaces for possibility of mocking
* Removed IMAP extension XFOCUS, used bridge local API instead
* Removed IMAP extension XSTOP
* Sentry is not used in dev environment
* Logs are not cleared after start, clearing is triggered by `watchLogFileSize()` instead
* Log path changed one folder level up i.e. from `.../protonmail/bridge/c10` to `.../protonmail/bridge`
* Always cleared malformed keychain records
* Set credentials version on each `Put`
* `util.WriteHeader` -> `imap.writeHeader`
* save `message.ExternalID` for every client not just AppleMail
* server errors reported to frontend by common event listener
* Handle logout in event loop
## [v1.1.6] - 2019-07-09 (beta 2019-07-01)
### Added
* #841 assume text/plain during sending e-mails when missing content type
* #805 list the new package links in upgrade dialog for linux
* #802 report the list errors to sentry
* #508 content related header fields for mail are saved in DB inside `msg.Header`
* #508 `doNotCacheError` to decide whether to rebuild message
* CI with lint check
* build flag `nogui`
* dummy html interface
### Removed
* #508 content type rewrite on `GetHeader`
* #508 content type on custom message
### Changed
* #854 avoid `nil` header and bodystructure on fail (as regression of #508)
* sanitize version in json file
* #850 keep correct main and body headers for import (as regression of #508)
* #841 choose parent ID only when there is exactly one message with external ID
* #811 #proton/backend-communication#11 go-pmapi!57 uid fixed
* update Qt 5.11.3 to 5.12.0
* using gomodules instead of glide
* #508 use MIMEType and attachments to choose correct `Content-Type`
* #508 custom message replaces body before header is created
* #508 main header has `Content-Type` only after message was fully fetched
* #770 ignore empty key from data card and support multiple keys for contacts
* Build tags for simpler build of beta and QA builds.
* lint corrections
## [v1.1.5] - 2019-05-23 (beta 2019-05-23, 2019-05-16)
### Changed
* fix custom message format
* #802 acumulated long lines while parsing body structure
* process `AddressEvent` before `MessageEvent`
* #791 updated crypto: fix wrong signature format
* #793 fix returning size
* #706 improved internet connection checking
* #771 updated raven, crypto, pmapi
* #792 use `INFO` as basic log level
* only one crash from second instance
* during event `MessageID` in log as field
## [v1.1.4 live] - 2019-04-10 (beta 2019-04-05, 2019-03-27)
### Added
* Address with port to IMAP debug
* #750 `backend/events.Manager.LastEvent`
* #750 `backend.user.areAllEventsProcessed`
* #750 Wait with message events until all related mailboxes are synchronized
* Restart limit to 10
* Release string to raven
### Changed
* #748 when charset missing assume utf8 and check the validity
* #750 before sync check that events are uptodate, if not poll events instead of sync
* Use pmapi with support of decrypted access token
* #750 Status is using DB status instead of API
* Format panic error as string instead of struct dump
* Validity of local certificate to increased to 20 years
### Removed
* #750 Synchronization after 450 messages
## [v1.1.3] - 2019-03-04
### Added
* sentry crash reporting in main
* program arguments to turn of CPU and memory profiling
* full version of program visible on release notes
### Changed
* #720 only one concurent DB sync
* #720 sync every 3 pages
* #512 extending list of charsets go-pm-mime!4
## [v1.1.2] - beta only 2019-02-21
### Changed
* #512 fail on unknown charset
* #729 #733 visitor for MIME parsing
## [v1.1.1] - 2019-02-11
### Added
* #671 include `name` param in attachment `Content-Type` (in addition to `Content-Disposition` param `filename`)
* #671 do not include content headers for section requests e.g. `BODY.PEEK[2]`
* version info checks for newer version (do not show dialog when older is online)
* #592 new header `X-Pm-Conversation-Id` and also added to `References`
* #666 invoke `panic` while adding account `jakubqa+crash@protonmail.com`
* #592 new header fields `X-Pm-Date` storing m.Time and `X-Pm-External-Id` storing m.ExternalID
* #484 search criteria `Unkeyword` support
### Changed
* fix srp modulus issue with new `ProtonMail/crypto`
* generate version files from main file
* be able to set update set on build
* #597 check on start that certificat will be still valid after one month and generate new cert if not
* #597 extended certificate validity to 2 years
* copyright 2019
* exclude `protontech` repos from credits
* refactor of `go-pmapi`, `go-pm-crypto`, `go-pm-mime` and `go-srp`
* re-signed pubkey key
* version, revision and build time is set in main
* #666 use `bytes.Reader` instead of `bytes.Buffer`
* #666 clear unused buffers in body structure map
* No API request for fetch `body[header]`
* Added crash file counter to pass log tests
* #484 search fully relies on DB information (no need to reach API)
* #592 `parsingHeader` allows negative time (before 1.1.1970)
* #592 add original header first and then rewrite
* #592 `Message-Id` rewritten only if not present
* #592 rename `X-Internal-Id` to `X-Pm-Internal-Id`
* #592 internal references are added only when not present already
* #592 field `Date` changed to m.Time only when wrong format or missing `Date`
* #645 pmapi#26 `Message.Flags` instead of `IsEncrypted`, `Type`, `IsReplied`, `IsRepliedAll`, `IsForwarded`
* DB: do not allow to put Body or Attachements to db
* #574 SMTP: can now send more than one email
* #671 Verbosity levels: `debug` (only bridge), `debug-client` (bridge and client communication), `debug-server` (bridge, whole SMTP/IMAP communication)
* #644 Return rfc.size 0 or correct size of fetched body (stored in DB)
* #671 API requests URI in debug logs
* #625 Fix search results for Flagged and Unflagged
* Draft optimization fetch header
* #656 Fix sending of calendar invite on Outlook on MacOS
* #46 Allowed to run multiple instances, once per user
### Removed
* makefile clean up old deploy code
### Release notes
• Support multiple Bridge instances running in parallel (one per user)
### Fixed bugs
• SMTP stays authenticated after sent message
• Reduce memory, processor and number of API calls
## [v1.1.0] - 2018-10-22
### Removed
* `go-pmapi.Config.ClientSecret`
* `go-pmapi.PublicKey.Send`
* program argument `main`
* `backend.getMIMEMessageBodySection` (use `message.BodySection`)
* `backend.getSize` (use `message.BodySection`)
### Added
* IMAP server: more info when write/send/flush error occurs #648
* linux package paths inside version-json
* draggable popup windows for outgoing non-encrypted message #519
* pmapi able to receive plain accessToken go-pmapi#23 #604
* DB debug switch
* clearing message cache when db is cleared
* debug string to tests
* mime tree section parsing and test
* start ticker
* dump DB status
* `backend.ApplicationOutdated()` mechanism: once triggered logout all email clients. On try to login say _application outdated_
* Force upgrade event (send from event loop)
* new systray with error symbol (used in mac for force update)
* test for upgrade
* GUI for upgrade
* add native upgrade to updates
* dial timeout client
* custom `copyRecursively` function
* when there is fresh version on start show release notes
* keychain helper using GNU pass
* error message on missing keychain
### Changed
* imap `SEARCH` loops until all messages are listed #581
* cached message timestamp is renewed on load
* message cache ID is userID+messageID
* private cache and added bodystructure
* Remove addresses from `m.ToList` that were not requested in SMTP `TO`
* IsFirstStart setup before loading Gui. Set it to false right after (don't wait till quit)
* check `eventMessage` not nil before address check
* `util.EventChannel` refactor: `SendEvent`->`Send` and new `SendEvent(EventCode)`
* Information bar keeps on once app is outdated
* Error dialog for upgrade has option for force upgrade
* IsFirstStart setup before loading Gui. Set false right after (don't wait till quit)
* pmapi: access token decrypted only if needed
* move `updates` from `frontend` to `util`
* move `CheckInternetConnection()` to `util`
* makefile clean up and change scripts for building
* reorganized updates
* start with new versioning
1.1.0
| | `--- bug fix number (internal, irregular, beta relases)
| `----- minor version (features, release once per month, live release, milestones)
`------- major version (big changes, once per year, breaking changes, api force upgrade)
* upgrade restart option in qt-frontend
* GOOS save functions
* windows update procedure
* darwin update procedure
* `zip` replaced by `tgz`
* using move instead of write truncated
* linux dependencies (pass and gnome-keychain optional)
* `Store.helper` -> `Store.secrets`
### Release notes
• New self-update procedure
• Changed restarting mechanism
• Support for GNU pass for linux
• Various GUI improvements
### Fixed bugs
• RFC complaint SEARCH and FETCH responses
• Additional synchronization of mail database
## [v1.0.6 silent] - 2018-08-23
### Added
* new svg icon in linux package
## [v1.0.6] - 2018-08-09
### Added
* `backend.GetUserSettings()`
### Changed
* related to Desktop-Bridge#561
* api flag to build scripts
* `BodyKey` and `AttachmentKey` contains `Key` and `Algorithm`
* `event.User.Addresses` -> `event.Addresses`
* `user.Addresses` -> `client.Addresses()`
* typos and fixes
* pmapi update
* `backend.configClient` -> `backend.authClient`
* `auth.Uid` -> `auth.Uid()`
* `keyRingForAddress()` -> `Client.KeyRingForAddressID()`
* `Message.IsRead` -> `Message.Unread`
* `pmapi.User.Unlock()` -> `pmapi.Client.UnlockAddresses()`
* `TwoFactor` -> `HasTwoFactor()` and `PasswordMode` -> `HasMailboxPassword()`
* icon to match ImportExport
### Release notes
• Removed deprecated API routes
### Fixed bugs
• Frequent Thunderbird timeout
• SMTP requests not case-sensitive
## [v1.0.5] - 2018-07-12
### Added
* UpdateCurrentAgent from lastMailClient
* current OS
* use Qt to set nice OS with version
* all `client.Do` errors are interpreted as connection issue
* moved to internal gitlab
* typo `frontend-qml`
* better message for case when server is not reacheable
* Setting 1min timeout to IMAP connection
### Changed
* password: click2show, click2hide
* notification in bug report window
* less frequent version check
* closing ticker
### Removed
* sockets and unused libraries
### Release notes
* Improved response of IMAP server
* Sending requests with client information
* Less frequent notification about new version
### Fixed bugs
* Support of Outlook calendar event format
* Too many opened file descriptors issue
* Fixed 7bit MIME issue while sending
## [v1.0.4] - 2018-05-15
### Changed
* version files available at both download and static
* MIME `text/calendar` are parsed as attachment
* UserID as identifier in keychain and pmapi token
* Keychain format and function refactor
* Create crash file on panic with full trace
* Clear old data only in main process (no double keychain typing)
* Create label udpate API route
* Selectable text in release notes
### Added
* Support sending to external PGP recipients
* Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Uknown argument`, `42: Restart application`
### Release notes
* Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients)
* Notification that outgoing email will be delivered as non-encrypted
* NOTE: Due to a change of the keychain format, you will need to add your account(s) to the Bridge after installing this version
### Bugs fixed
* Support accounts with same user names
* Support sending vCalendar event
## [v1.0.3] - 2018-03-26
* All from silent updates plus following
### Changed
* Okay -> "Remind me later"
* Imported message with `text/html` body was imported as `text/plain`
* Reload cache when labeling Seen/Unseen
* Merge with Import-Export branch
* Inheritable Bug report window
* Common functions: WriteHeader (parse PM mail) and CustomMessage (when incorrect keys)
* Updates refactor
* Bug report window
* Checkbox and with label (only I/E)
* Error dialog and Info tooltip (only I/E)
* Add user modal formating (colors, text)
* Account view style
* Input box style (used in bug report)
* Input field style (used in add account and change port)
* Added style variables for I/E
* Tab button style
* Window bar style and functionality (closing / minimize window)
### Release notes
* Improved responsiveness in the UI
### Fixed bugs
* Fixed some formatting issues with imports
* Fixed port changing via CLI
## Silent update - 2018-03-13
### Changed
* Remove firewall error message
## [v1.0.2] - 2018-03-12
* All from silent updates plus following
### Added
* UTF-7 support
* Message when communication between bridge and email client is blocked by firewall (Windows only)
### Changed
* Added gnome-keyring dejavu fonts into linux dependency
* Corrected parentID when reply/forward: taken from `protonmail.internalid` reference
* Update user object in backend after unlock to apply address changes
### Release notes
* UTF7 encoding support for older imported emails
### Fixed bugs
* Fixed issues with conversation threading
* Support for multiple "ReplyTo" addresses
* Fixed issue where some address updates would not register immediately
## [v1.0.1-4 (linux only)] Silent deploy - 2018-02-28
### Changed
* More similar look of window title bar to Windows 10 style.
* Qt 5.10 Button Controls 2 conflict (`icon`->`iconText`)
### Added
* Linux default font
* Multiple reply-to addresses support (also API)
* Command line interface
* Credits are generated automatically from glide.lock
* Created script to build linux packages (dep,rpm,PKGBUILD)
* Correct config folders with env variable `$XDG_CONFIG_HOME`
### Fixed bugs
* Clearing global cache
* Default linux font problems
* Support Reply-To multiple addresses
### Release notes
* Improved visual appearance for win and linux
## [v1.0.1] Silent deploy - 2017-12-30
### Changed
* Fixed bug with parsing address list (CC became BCC)
## [v1.0.1] - 2017-12-20
### Added
* When current log file is more than 10MB open new one, checked every 15min
* Keep only last three log files including current one, triggered every start and when switching log files
* Translation context
* Accessibility objects for button and static text
* All objects are accessible including focus scope for modals and messages
* Automatically fill the email client in bug report form
* Catch corrupted MacOS keychain error and show the link to FAQ
* Unlabel message
* Update emptying and filtering routes
* Parse the address comment as defined in RFC
### Changed
* Default log level set to Warning
* Info logs during adding account and connecting client promoted to warning level
* Log only when email client was changed (previously logged on every assign)
* Force upgrade bubble notification only when requested by API
* Don't show warning systray icon when "You have then newest version!" bubble message is showed
* Header date format RFC822Z -> RFC1123Z
* IMAP ID and QUOTA responses forced to use quoted strings (fixing SparkMail issue)
* Avoid AddressChanged bubble when no address was changed
### Release notes
* Reduced log file size and log file history
* Accessibility support for MacOS VoiceOver and Windows Narrator
* Improved notification system
* Supported imports with older address format
## [v1.0.0] - 2017-12-06
### Added
* Encoding support of message body, title items, attachment name, for all standard charsets
* Force update API message handled as new version event
### Changed
* Refactor `bridge-qtfronted` -> `frontend`
* Only one main file and basic support of CLI (not finished)
* Common QML package `ProtonUI`, which is used by `BridgeUI` and `ImportExportUI`
* ChangedUser signal contain address and event type to distinguish between logout, internet off/on, address_change
* API address changed event handled gracefully (if not possible, logout)
* Update mac keychain (should resolve problem with adding new account to bridge, NOT CONFIRMED)
* Solved hanging GUI on keychain error (should solve all win-7, no-gui errors)
* New systray icons for Mac (black and white no background)
* GUI cosmetics:
- "Click here to start" triangle position
- Wrong cursor type on link
- Create main window before notification
### Release notes
* Better notification when new version is needed or when account address is changed.
* Encoding support for the standard charsets.
* Improved visual appearance.
### Fixed bugs
* Fixed missing GUI for Windows with empty keychain.
## Changelog format
* Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Guiding Principles
* Changelogs are for humans, not machines.
* There should be an entry for every single version.
* The same types of changes should be grouped.
* Versions and sections should be linkable.
* The latest version comes first.
* The release date of each version is displayed.
* Mention whether you follow Semantic Versioning.
### Types of changes
* `Added` for new features.
* `Changed` for changes in existing functionality.
* `Deprecated` for soon-to-be removed features.
* `Removed` for now removed features.
* `Fixed` for any bug fixes.
* `Security` in case of vulnerabilities.
* additional for in app release notes
* `Release notes` in case of vulnerabilities.
* `Fixed bugs` in case of vulnerabilities.

621
LICENSE Normal file
View File

@ -0,0 +1,621 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS

208
Makefile Normal file
View File

@ -0,0 +1,208 @@
export GO111MODULE=on
GOOS:=$(shell go env GOOS)
## Build
.PHONY: build check-has-go
VERSION?=1.2.6-git
REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z)
BUILD_TAGS?=pmapi_prod
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
GO_LDFLAGS:=$(addprefix -X main.,Version=${VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+= ${BUILD_LDFLAGS}
endif
GO_LDFLAGS:=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS+= ${GO_LDFLAGS}
BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
DEPLOY_DIR:=cmd/Desktop-Bridge/deploy
ICO_FILES:=
EXE:=$(shell basename ${CURDIR})
ifeq "${GOOS}" "windows"
EXE+=.exe
ICO_FILES:=logo.ico icon.rc icon_windows.syso
endif
ifeq "${GOOS}" "darwin"
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
EXE:=Contents/Macos/${EXE}
endif
EXE_TARGET:=${DEPLOY_DIR}/${GOOS}/${EXE}
TGZ_TARGET:=bridge_${GOOS}_${REVISION}.tgz
build: ${TGZ_TARGET}
${TGZ_TARGET}: ${DEPLOY_DIR}/${GOOS}
rm -f $@
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${GOOS}
${DEPLOY_DIR}/linux: ${EXE_TARGET}
cp -pf ./internal/frontend/share/icons/logo.svg ${DEPLOY_DIR}/linux/
cp -pf ./LICENSE ${DEPLOY_DIR}/linux/
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
${DEPLOY_DIR}/darwin: ${EXE_TARGET}
cp ./internal/frontend/share/icons/Bridge.icns ${DARWINAPP_CONTENTS}/Resources/
cp -r "utils/addcert.scpt" ${DARWINAPP_CONTENTS}/Resources/
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
${DEPLOY_DIR}/windows: ${EXE_TARGET}
cp ./internal/frontend/share/icons/logo.ico ${DEPLOY_DIR}/windows/
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
rm -rf deploy ${GOOS} ${DEPLOY_DIR}
cp cmd/Desktop-Bridge/main.go .
qtdeploy ${BUILD_FLAGS} build desktop
mv deploy cmd/Desktop-Bridge
rm -rf ${GOOS} main.go
logo.ico: ./internal/frontend/share/icons/logo.ico
cp $^ .
icon.rc: ./internal/frontend/share/icon.rc
cp $^ .
./internal/frontend/qt/icon_windows.syso: ./internal/frontend/share/icon.rc logo.ico
windres $< $@
icon_windows.syso: ./internal/frontend/qt/icon_windows.syso
cp $^ .
## Rules for therecipe/qt
.PHONY: prepare-vendor update-vendor
THERECIPE_QTVER:=$(shell grep "github.com/therecipe/qt " go.mod | sed -r 's;.* v[0-9\.]+-[0-9]+-([a-f0-9]*).*;\1;')
THERECIPE_ENV:=github.com/therecipe/env_${GOOS}_amd64_513
# vendor folder will be deleted by gomod hence we cache the big repo
# therecipe/env in order to download it only once
vendor-cache/${THERECIPE_ENV}:
git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV}
LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV}
ifeq "${GOOS}" "windows"
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
LINKCMD:=cmd //c 'mklink $(subst /,\,vendor\${THERECIPE_ENV} ${WINDIR})'
endif
prepare-vendor:
go install -v -tags=no_env github.com/therecipe/qt/cmd/...
go mod vendor
# update-vendor is PHONY because we need to make sure that we always have updated vendor
update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
${LINKCMD}
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated
LINTVER:="v1.23.6"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
install-devel-tools: check-has-go
go get -v github.com/golang/mock/gomock
go get -v github.com/golang/mock/mockgen
go get -v github.com/go-delve/delve
install-linter: check-has-go
curl -sfL $(LINTSRC) | sh -s -- -b $(shell go env GOPATH)/bin $(LINTVER)
install-go-mod-outdated:
which go-mod-outdated || go get -u github.com/psampaz/go-mod-outdated
## Checks, mocks and docs
.PHONY: check-has-go check-license test bench coverage mocks lint updates doc
check-has-go:
@which go || (echo "Install Go-lang!" && exit 1)
check-license:
find . -not -path "./vendor/*" -not -name "*mock*.go" -regextype posix-egrep -regex ".*\.go|.*\.qml" -exec grep -L "Copyright (c) 2020 Proton Technologies AG" {} \;
test: gofiles
@# 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} \
./internal/api/... \
./internal/bridge/... \
./internal/events/... \
./internal/frontend/autoconfig/... \
./internal/frontend/cli/... \
./internal/imap/... \
./internal/preferences/... \
./internal/smtp/... \
./internal/store/... \
./pkg/...
bench:
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
go tool pprof -png -output bench_mem.png bench_mem.pprof
go tool pprof -png -output bench_cpu.png bench_cpu.pprof
coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/bridge Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer > internal/bridge/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
lint:
which golangci-lint || $(MAKE) install-linter
golangci-lint run ./...
updates: install-go-mod-outdated
# Uncomment the "-ci" to fail the job if something can be updated.
go list -u -m -json all | go-mod-outdated -update -direct #-ci
doc:
godoc -http=:6060
.PHONY: gofiles
# Following files are for the whole app so it makes sense to have them in bridge package.
# (Options like cmd or internal were considered and bridge package is the best place for them.)
gofiles: ./internal/bridge/credits.go ./internal/bridge/release_notes.go
./internal/bridge/credits.go: ./utils/credits.sh go.mod
cd ./utils/ && ./credits.sh
./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes.txt ./release-notes/bugs.txt
cd ./utils/ && ./release-notes.sh
## Run and debug
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug qmlpreview qt-fronted-clean clean
VERBOSITY?=debug-client
RUN_FLAGS:=-m -l=${VERBOSITY}
run: run-nogui-cli
run-qt: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} | tee last.log
run-qt-cli: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
run-nogui: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} | tee last.log
run-nogui-cli: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} -c
run-debug:
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/Desktop-Bridge/main.go -- ${RUN_FLAGS}
run-qml-preview:
make -C internal/frontend/qt -f Makefile.local qmlpreview
clean-frontend-qt:
make -C internal/frontend/qt -f Makefile.local clean
clean-vendor: clean-frontend-qt
rm -rf ./vendor
clean: clean-frontend-qt
rm -rf vendor-cache
rm -rf cmd/Desktop-Bridge/deploy
rm -f build last.log mem.pprof

52
README.md Normal file
View File

@ -0,0 +1,52 @@
# ProtonMail Bridge
Copyright (c) 2020 Proton Technologies AG
This repository holds the ProtonMail Bridge application.
For a detailed build information see [BUILD](./BUILD.md).
For licensing information see [COPYING](./COPYING.md).
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
## Description
ProtonMail Desktop Bridge for e-mail clients.
When launched, the servers will be started and a GUI will show up. From this GUI,
the server can be started and stopped and configuration for e-mail clients can
be generated.
To configure an e-mail client, enter your ProtonMail credentials. Open your
e-mail client and add a new account with the settings which are displayed.
The client will be able to sync with your ProtonMail account only when the
bridge is started, so enabling it on startup is recommended.
When the main window is closed, the bridge will continue to run in the
background.
More details [on the public website](https://protonmail.com/bridge).
## Keychain
You need to have keychain in order to run the ProtonMail Bridge. On Mac or
Windows, Bridge uses native credential managers. On Linux, use
[Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/)
or
[pass](https://www.passwordstore.org/).
## Environment Variables
### Bridge application
- `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
- `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.
- `VERSION`: set the bridge app version used during testing or building.
### Integration testing
- `TEST_ENV`: set which env to use (fake or live)
- `TEST_ACCOUNTS`: set JSON file with configured accounts
- `TAGS`: set build tags for tests
- `FEATURES`: set feature dir, file or scenario to test

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

434
cmd/Desktop-Bridge/main.go Normal file
View File

@ -0,0 +1,434 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package main
/*
___....___
^^ __..-:'':__:..:__:'':-..__
_.-:__:.-:'': : : :'':-.:__:-._
.':.-: : : : : : : : : :._:'.
_ :.': : : : : : : : : : : :'.: _
[ ]: : : : : : : : : : : : : :[ ]
[ ]: : : : : : : : : : : : : :[ ]
:::::::::[ ]:__:__:__:__:__:__:__:__:__:__:__:__:__:[ ]:::::::::::
!!!!!!!!![ ]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![ ]!!!!!!!!!!!
^^^^^^^^^[ ]^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[ ]^^^^^^^^^^^
[ ] [ ]
[ ] [ ]
jgs [ ] [ ]
~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^
*/
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/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/urfave/cli"
)
// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
// Different number will drop old files and create new ones.
const cacheVersion = "c11"
// Following variables are set via ldflags during build.
var (
// Version of the build.
Version = "" //nolint[gochecknoglobals]
// Revision is current hash of the build.
Revision = "" //nolint[gochecknoglobals]
// BuildTime stamp of the build.
BuildTime = "" //nolint[gochecknoglobals]
// AppShortName to make setup
AppShortName = "bridge" //nolint[gochecknoglobals]
// DSNSentry client keys to be able to report crashes to Sentry
DSNSentry = "" //nolint[gochecknoglobals]
)
var (
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() {
if err := raven.SetDSN(DSNSentry); err != nil {
log.WithError(err).Errorln("Can not setup sentry DSN")
}
raven.SetRelease(Revision)
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
}
config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r))
frontend.HandlePanic()
*ph.err = cli.NewExitError("Panic and restart", 666)
numberOfCrashes++
log.Error("Restarting after panic")
restartApp()
os.Exit(666)
}
// run initializes and starts everything in a precise order.
//
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(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)
}
}
}

135
doc/bridge.md Normal file
View File

@ -0,0 +1,135 @@
# Bridge
## Main blocks
This is basic overview of the main bridge blocks.
Note connection between IMAP/SMTP and PMAPI. IMAP and SMTP packages are in the queue to be refactored
and we would like to try to have functionality in bridge core or bridge utilities (such as messages)
than direct usage of PMAPI from IMAP or SMTP. Also database (BoltDB) should be moved to bridge core.
```mermaid
graph LR
S[Server]
C[Client]
U[User]
subgraph "Bridge app"
Core[Bridge core]
API[PMAPI]
Store
DB[BoltDB]
Frontend["Qt / CLI"]
IMAP
SMTP
IMAP --> Store
IMAP --> Core
SMTP --> Core
SMTP --> API
Core --> API
Core --> Store
Store --> API
Store --> DB
Frontend --> Core
end
C --> IMAP
C --> SMTP
U --> Frontend
API --> S
```
## Code structure
More detailed graph of main types used in Bridge app and connection between them. Here is already
communication to PMAPI only from bridge core which is not true, yet. IMAP and SMTP are still calling
PMAPI directly.
```mermaid
graph TD
C["Client (e.g. Thunderbird)"]
PM[ProtonMail Server]
subgraph "Bridge app"
subgraph "Bridge core"
B[Bridge]
U[User]
B --> U
end
subgraph Store
StoreU[Store User]
StoreA[Address]
StoreM[Mailbox]
StoreU --> StoreA
StoreA --> StoreM
end
subgraph Credentials
CredStore[Store]
Creds[Credentials]
CredStore --> Creds
end
subgraph Frontend
CLI
Qt
end
subgraph IMAP
IB[IMAP backend]
IA[IMAP address]
IM[IMAP mailbox]
IB --> B
IB --> IA
IA --> IM
IA --> U
IA --> StoreA
IM --> StoreM
end
subgraph SMTP
SB[SMTP backend]
SS[SMTP session]
SB --> B
SB --> SS
SS --> U
end
end
subgraph PMAPI
AC[Client]
end
C --> IB
C --> SB
CLI --> B
Qt --> B
U --> CredStore
U --> Creds
U --> StoreU
StoreU --> AC
StoreA --> AC
StoreM --> AC
B --> AC
U --> AC
AC --> PM
```
## How to debug
Run `make run-debug` which starts [Delve](https://github.com/go-delve/delve).

114
doc/communication.md Normal file
View File

@ -0,0 +1,114 @@
# Communication
## First login and sync
When user logs in to the bridge for the first time, immediatelly starts the first sync.
First sync downloads all headers of all e-mails and creates database to have proper UIDs
and indexes for IMAP. See [database](database.md) for more information.
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
and lables) without need to download each e-mail headers many times.
Note that we need to download also bodies to calculate size of the e-mail and set proper
content type (clients uses content type for guess if e-mail contains attachment)--but only
body, not attachment. Also it's downloaded only for the first time. After that we store
those information in our database so next time we only sync headers, labels and so on.
First sync takes some time. List of 150 messages takes about second and then we need to
download bodies for each message. We still need to do some optimalizations. Anyway, if
user has reasonable amount of e-mails, there is good chance user will see e-mails in the
client right after adding account.
When account is added to client, client start the sync. This sync will ask Bridge app
for all headers (done quickly) and then starts to download all bodies and attachment.
Unfortunatelly for some e-mail more than once if the same e-mail is in more mailboxes
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
servers (each 30 seconds) for new updates (new message, keys, …).
```mermaid
sequenceDiagram
participant S as Server
participant B as Bridge
participant C as Client
Note right of B: Set up PM account<br/>by user
loop First sync
B ->> S: Fetch body and attachements
Note right of B: Build local database<br/>(e-mail UIDs)
end
Note right of C: Set up IMAP/SMTP<br/>by user
C ->> B: IMAP login
B ->> S: Authenticate user
Note right of B: Create IMAP user
loop Event loop, every 30 sec
B ->> S: Fetch e-mail headers
B ->> C: Send IMAP IDLE response
end
C ->> B: IMAP LIST directories
loop Client sync
C ->> B: IMAP SELECT directory
C ->> B: IMAP SEARCH e-mails UIDs
C ->> B: IMAP FETCH of e-mail UID
B ->> S: Fetch body and attachements
Note right of B: Decrypt message<br/>and attachement
B ->> C: IMAP response
end
```
## IMAP IDLE extension
IMAP IDLE is extension, it has to be supported by both client and server. IMAP server (in our case
the bridge) supports it so clients can use it. It works by issuing `IDLE` command by the client and
keeps the connection open. When the server has some update, server (the bridge) will respond to that
by `EXISTS` (new message), `APPEND` (imported message), `EXPUNGE` (deleted message) or `MOVE` response.
Even when there is connection with IDLE open, server can mark the client as inactive. Therefore,
it's recommended the client should reissue the connection after each 29 minutes. This is not the
real push and can fail!
Our event loop is also simple pull and it will trigger IMAP IDLE when we get some new update from
the server. Would be good to have push from the server, but we need to wait for the support on API.
RFC: https://tools.ietf.org/html/rfc2177
```mermaid
sequenceDiagram
participant S as Server
participant B as Bridge
participant C as Client
C ->> B: IMAP IDLE
loop Every 30 seconds
S ->> B: Checking events
B ->> C: IMAP response
end
```
## Sending e-mails
E-mail are sent over standard SMTP protocol. Our bridge takes the message, encrypts and sent it
further to our server which will then send the message to its final destination. The important
and tricky part is encryption. See [encryption](encryption.md) or [PMEL document](https://docs.google.com/document/d/1lEBkG0DC5FOWlumInKtu4a9Cc1Eszp48ZhFy9UpPQso/edit)
for more information.
```mermaid
sequenceDiagram
participant S as Server
participant B as Bridge
participant C as Client
C ->> B: SMTP send e-mail
Note right of B: Encrypt messages
B ->> S: Send encrypted e-mail
B ->> C: Respond OK
```

27
doc/database.md Normal file
View File

@ -0,0 +1,27 @@
# Database
Bridge needs to have a small database to pair our IDs with IMAP UIDs and indexes. IMAP protocol
requires every message to have an unique UID in mailbox. In this context, mailbox is not an account,
but a folder or label. This means that one message can have more UIDs, one for each mailbox (folder),
and that two messages can have the same UID, but each for different mailbox (folder).
IMAP index is just an index. Look at it like to an array: `["UID1", "UID2", "UID3"]`. We can access
message by UID or index; for example index 2 and UID `UID2`. When this message is deleted, we need
to re-index all following messages. The array will look now like `["UID1", "UID3"]` and the last
message can be accessed by index 2 or UID `UID3`.
See RFCs for more information:
* https://tools.ietf.org/html/rfc822
* https://tools.ietf.org/html/rfc3501
Our database is currently built on BBolt and have those buckets (key-value storage):
* Message metadata bucket:
* `[metadataBucket][API_ID] -> pmapi.Message{subject, from, to, size, other headers...}` (without body or attachment)
* Mapping buckets
* `[mailboxesBucket][addressID-mailboxID][api_ids][API_ID] -> UID`
* `[mailboxesBucket][addressID-mailboxID][imap_ids][UID] -> API_ID`

12
doc/encryption.md Normal file
View File

@ -0,0 +1,12 @@
# Encryption
Encryption is done in PMAPI, bridge utils and bridge itself. The best would be to keep encryption
in PMAPI and bridge utils (in pacakge such as messages). All packages are using our high-level
GopenPGP library on top of openpgp.
## `gopenpgp.KeyRing`
We use one `KeyRing` per address. Our usage then contains all keys for specific address. Primary
key is always on the first position, then there old ones to be able to decrypt last e-mail.
Openpgp encrypts given message with all available keys, so we need to first get first (primary)
key for encryption to have message encrypted only once with primary key.

9
doc/index.md Normal file
View File

@ -0,0 +1,9 @@
# Documentation
Documentation pages in order to read for a novice:
* [Development cycle](development.md)
* [Bridge code](bridge.md)
* [Internal Bridge database](database.md)
* [Communication between Bridge, Client and Server](communication.md)
* [Encryption](encryption.md)

77
go.mod Normal file
View File

@ -0,0 +1,77 @@
module github.com/ProtonMail/proton-bridge
go 1.13
// These dependencies are `replace`d below, so the version numbers should be ignored.
// They are in a separate require block to highlight this.
require (
github.com/docker/docker-credential-helpers v0.0.0-00010101000000-000000000000
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
)
require (
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
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-imap-id v0.0.0-20171219160728-ed0baee567ee
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
github.com/andybalholm/cascadia v1.1.0
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cucumber/godog v0.8.1
github.com/danieljoos/wincred v1.0.2 // indirect
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
github.com/fatih/color v1.9.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/getsentry/raven-go v0.2.0
github.com/go-resty/resty/v2 v2.2.0
github.com/golang/mock v1.4.3
github.com/google/go-cmp v0.4.0
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
github.com/hashicorp/go-multierror v1.0.0
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195
github.com/jhillyerd/enmime v0.8.0
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
github.com/miekg/dns v1.1.29
github.com/myesui/uuid v1.0.0 // indirect
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.4.2
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.5.1
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
github.com/twinj/uuid v1.0.0 // indirect
github.com/urfave/cli v1.22.3
go.etcd.io/bbolt v1.3.3
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 (
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-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
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f
)

228
go.sum Normal file
View File

@ -0,0 +1,228 @@
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f h1:cFhATQTJGK2iZ0dc+jRhr75mh6bsc5Ug6NliaBya8Kw=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
github.com/ProtonMail/docker-credential-helpers v1.0.0 h1:0DQXbZNvUszWgXUuP7TzvQdwnkK1D5Zf/glBgCFJFCk=
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/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f h1:QkLm4yfhBQuBxrC46Vhy2sonOWVrwIJo5bgKpA82+TY=
github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f/go.mod h1:+m2uLXghuYktgE/vc5AkmCxx1qhu33ZKHFWg1cGZPD0=
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee h1:Q/nK7A9xzUimAZsQDa/yaw3xW9PkTTnJnkT5wAkXrmI=
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de h1:+LA9teDYUwGkBvg0kqZPZetmxIv1r7s9/npBP1yzKs0=
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-20190521135552-09454e3dbe72 h1:hGCc4Oc2fD3I5mNnZ1VlREncVc9EXJF8dxW3sw16gWM=
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/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/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed h1:3gib6hGF61VfRu7cqqkODyRUgES5uF/fkLQanPPJiO8=
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed/go.mod h1:NstNbZx1OIoyq+2qHAFLwDFpHbMk8L2i2Vr+LioJ3/g=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/aslakhellesoy/gox v1.0.100/go.mod h1:AJl542QsKKG96COVsv0N74HHzVQgDIQPceVUh1aeU2M=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cucumber/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/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/cucumber/godog v0.9.0 h1:QOb8wyC7f+FVFXzY3RdgowwJUb4WeJfqbnQqaH4jp+A=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42 h1:3TeZ5gy3We/LVL0sqmGhM8dFDTSM7Hyj7PMIdl6OTs4=
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89 h1:AzbVhcrxgJO5MfSvzG5q4IfrYVm0Jw4AHNPz47+DiR0=
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI=
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/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195 h1:j0UEFmS7wSjAwKEIkgKBn8PRDfjcuggzr93R9wk53nQ=
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.8.0 h1:PHc/2LXtnDmCDm0V4+5NlBx+MoubmufhuNXwpKSV2o8=
github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2 h1:1XZArHAPddaXKbg51etNbCjkNUkKgSa0s8dSz2LYB2g=
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.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/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.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.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6 h1:qsqscDgSJy+HqgMTR+3NwjYJBbp1+honwDsszLoS+pA=
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/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/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
icon.iconset/icon_16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
icon.iconset/icon_32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

94
internal/api/api.go Normal file
View File

@ -0,0 +1,94 @@
// 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 api provides HTTP API of the Bridge.
//
// API endpoints:
// * /focus, see focusHandler
package api
import (
"crypto/tls"
"fmt"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
)
var (
log = config.GetLogEntry("api") //nolint[gochecknoglobals]
)
type apiServer struct {
host string
pref *config.Preferences
tls *tls.Config
certPath string
keyPath string
eventListener listener.Listener
}
// NewAPIServer returns prepared API server struct.
func NewAPIServer(pref *config.Preferences, tls *tls.Config, certPath, keyPath string, eventListener listener.Listener) *apiServer { //nolint[golint]
return &apiServer{
host: bridge.Host,
pref: pref,
tls: tls,
certPath: certPath,
keyPath: keyPath,
eventListener: eventListener,
}
}
// Starts the server.
func (api *apiServer) ListenAndServe() {
mux := http.NewServeMux()
mux.HandleFunc("/focus", wrapper(api, focusHandler))
addr := api.getAddress()
server := &http.Server{
Addr: addr,
Handler: mux,
TLSConfig: api.tls,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}
log.Info("API listening at ", addr)
if err := server.ListenAndServeTLS(api.certPath, api.keyPath); err != nil {
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
log.Error("API failed: ", err)
}
defer server.Close() //nolint[errcheck]
}
func (api *apiServer) getAddress() string {
port := api.pref.GetInt(preferences.APIPortKey)
newPort := ports.FindFreePortFrom(port)
if newPort != port {
api.pref.SetInt(preferences.APIPortKey, newPort)
}
return getAPIAddress(api.host, newPort)
}
func getAPIAddress(host string, port int) string {
return fmt.Sprintf("%s:%d", host, port)
}

51
internal/api/ctx.go Normal file
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 api
import (
"net/http"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
// httpHandler with Go's Response and Request.
type httpHandler func(http.ResponseWriter, *http.Request)
// handler with our context.
type handler func(handlerContext) error
type handlerContext struct {
req *http.Request
resp http.ResponseWriter
eventListener listener.Listener
}
func wrapper(api *apiServer, callback handler) httpHandler {
return func(w http.ResponseWriter, req *http.Request) {
ctx := handlerContext{
req: req,
resp: w,
eventListener: api.eventListener,
}
err := callback(ctx)
if err != nil {
log.Error("API callback of ", req.URL, " failed: ", err)
http.Error(w, err.Error(), 500)
}
}
}

55
internal/api/focus.go Normal file
View File

@ -0,0 +1,55 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package api
import (
"crypto/tls"
"fmt"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
)
// focusHandler should be called from other instances (attempt to start bridge
// for the second time) to get focus in the currently running instance.
func focusHandler(ctx handlerContext) error {
log.Info("Focus from other instance")
ctx.eventListener.Emit(events.SecondInstanceEvent, "")
fmt.Fprintf(ctx.resp, "OK")
return nil
}
// CheckOtherInstanceAndFocus is helper for new instances to check if there is
// already a running instance and get it's focus.
func CheckOtherInstanceAndFocus(port int, tls *tls.Config) error {
transport := &http.Transport{TLSClientConfig: tls}
client := &http.Client{Transport: transport}
addr := getAPIAddress(bridge.Host, port)
resp, err := client.Get("https://" + addr + "/focus")
if err != nil {
return err
}
defer resp.Body.Close() //nolint[errcheck]
if resp.StatusCode != 200 {
log.Error("Focus error: ", resp.StatusCode)
}
return nil
}

510
internal/bridge/bridge.go Normal file
View File

@ -0,0 +1,510 @@
// 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 provides core business logic providing API over credentials store and PM API.
package bridge
import (
"errors"
"strconv"
"strings"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
m "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/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/hashicorp/go-multierror"
logrus "github.com/sirupsen/logrus"
)
var (
log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
isApplicationOutdated = false //nolint[gochecknoglobals]
)
// Bridge is a struct handling users.
type Bridge struct {
config Configer
pref PreferenceProvider
panicHandler PanicHandler
events listener.Listener
version string
pmapiClientFactory PMAPIProviderFactory
credStorer CredentialsStorer
storeCache *store.Cache
// users is a list of accounts that have been added to bridge.
// They are stored sorted in the credentials store in the order
// that they were added to bridge chronologically.
// People are used to that and so we preserve that ordering here.
users []*User
// idleUpdates is a channel which the imap backend listens to and which it uses
// to send idle updates to the mail client (eg thunderbird).
// The user stores should send idle updates on this channel.
idleUpdates chan interface{}
lock sync.RWMutex
userAgentClientName string
userAgentClientVersion string
userAgentOS string
}
func New(
config Configer,
pref PreferenceProvider,
panicHandler PanicHandler,
eventListener listener.Listener,
version string,
pmapiClientFactory PMAPIProviderFactory,
credStorer CredentialsStorer,
) *Bridge {
log.Trace("Creating new bridge")
b := &Bridge{
config: config,
pref: pref,
panicHandler: panicHandler,
events: eventListener,
version: version,
pmapiClientFactory: pmapiClientFactory,
credStorer: credStorer,
storeCache: store.NewCache(config.GetIMAPCachePath()),
idleUpdates: make(chan interface{}),
lock: sync.RWMutex{},
}
// Allow DoH before starting bridge if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if pref.GetBool(preferences.AllowProxyKey) {
AllowDoH()
}
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()
return b
}
// heartbeat sends a heartbeat signal once a day.
func (b *Bridge) heartbeat() {
for range time.NewTicker(1 * time.Hour).C {
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
if err != nil {
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))
}
}
}
func (b *Bridge) loadUsersFromCredentialsStore() (err error) {
b.lock.Lock()
defer b.lock.Unlock()
userIDs, err := b.credStorer.List()
if err != nil {
return
}
for _, userID := range userIDs {
l := log.WithField("user", userID)
apiClient := b.pmapiClientFactory(userID)
user, newUserErr := newUser(b.panicHandler, userID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
if newUserErr != nil {
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
continue
}
b.users = append(b.users, user)
if initUserErr := user.init(b.idleUpdates, apiClient); initUserErr != nil {
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
}
}
return err
}
func (b *Bridge) watchBridgeOutdated() {
ch := make(chan string)
b.events.Add(events.UpgradeApplicationEvent, ch)
for range ch {
isApplicationOutdated = true
b.closeAllConnections()
}
}
func (b *Bridge) closeAllConnections() {
for _, user := range b.users {
user.closeAllConnections()
}
}
// Login authenticates a user.
// The login flow:
// * Authenticate user:
// client, auth, err := bridge.Authenticate(username, password)
//
// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
// auth2FA, err := client.Auth2FA(twoFactorCode)
//
// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
// and then finish the login procedure.
// user, err := bridge.FinishLogin(client, auth, mailboxPassword)
func (b *Bridge) Login(username, password string) (loginClient PMAPIProvider, auth *pmapi.Auth, err error) {
log.WithField("username", username).Trace("Logging in to bridge")
b.crashBandicoot(username)
// We need to use "login" client because we need userID to properly
// assign access tokens into token manager.
loginClient = b.pmapiClientFactory("login")
authInfo, err := loginClient.AuthInfo(username)
if err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
return nil, nil, err
}
if auth, err = loginClient.Auth(username, password, authInfo); err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth for user")
return loginClient, auth, err
}
return loginClient, auth, nil
}
// FinishLogin finishes the login procedure and adds the user into the credentials store.
// See `Login` for more details of the login flow.
func (b *Bridge) FinishLogin(loginClient PMAPIProvider, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
log.Trace("Finishing bridge login")
defer func() {
if err == pmapi.ErrUpgradeApplication {
b.events.Emit(events.UpgradeApplicationEvent, "")
}
}()
b.lock.Lock()
defer b.lock.Unlock()
mbPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
if err != nil {
log.WithError(err).Error("Could not hash mailbox password")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after hash password failed.")
}
return
}
if _, err = loginClient.Unlock(mbPassword); err != nil {
log.WithError(err).Error("Could not decrypt keyring")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after unlock failed.")
}
return
}
apiUser, err := loginClient.CurrentUser()
if err != nil {
log.WithError(err).Error("Could not get login API user")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
}
return
}
user, hasUser := b.hasUser(apiUser.ID)
// If the user exists and is logged in, we don't want to do anything.
if hasUser && user.IsConnected() {
err = errors.New("user is already logged in")
log.WithError(err).Warn("User is already logged in")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Warn("Could not discard auth generated during second login")
}
return
}
apiToken := auth.UID() + ":" + auth.RefreshToken
apiClient := b.pmapiClientFactory(apiUser.ID)
auth, err = apiClient.AuthRefresh(apiToken)
if err != nil {
log.WithError(err).Error("Could refresh token in new client")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Warn("Could not discard auth generated after auth refresh")
}
return
}
// We load the current user again because it should now have addresses loaded.
apiUser, err = apiClient.CurrentUser()
if err != nil {
log.WithError(err).Error("Could not get current API user")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
}
return
}
apiToken = auth.UID() + ":" + auth.RefreshToken
activeEmails := apiClient.Addresses().ActiveEmails()
if _, err = b.credStorer.Add(apiUser.ID, apiUser.Name, apiToken, mbPassword, activeEmails); err != nil {
log.WithError(err).Error("Could not add user to credentials store")
return
}
// If it's a new user, generate the user object.
if !hasUser {
user, err = newUser(b.panicHandler, apiUser.ID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
if err != nil {
log.WithField("user", apiUser.ID).WithError(err).Error("Could not create user")
return
}
}
// Set up the user auth and store (which we do for both new and existing users).
if err = user.init(b.idleUpdates, apiClient); err != nil {
log.WithField("user", user.userID).WithError(err).Error("Could not initialise user")
return
}
if !hasUser {
b.users = append(b.users, user)
b.SendMetric(m.New(m.Setup, m.NewUser, m.NoLabel))
}
b.events.Emit(events.UserRefreshEvent, apiUser.ID)
return user, err
}
// GetUsers returns all added users into keychain (even logged out users).
func (b *Bridge) GetUsers() []*User {
b.lock.RLock()
defer b.lock.RUnlock()
return b.users
}
// GetUser returns a user by `query` which is compared to users' ID, username
// or any attached e-mail address.
func (b *Bridge) GetUser(query string) (*User, error) {
b.crashBandicoot(query)
b.lock.RLock()
defer b.lock.RUnlock()
for _, user := range b.users {
if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
return user, nil
}
for _, address := range user.GetAddresses() {
if strings.EqualFold(address, query) {
return user, nil
}
}
}
return nil, errors.New("user " + query + " not found")
}
// ClearData closes all connections (to release db files and so on) and clears all data.
func (b *Bridge) ClearData() error {
var result *multierror.Error
for _, user := range b.users {
if err := user.Logout(); err != nil {
result = multierror.Append(result, err)
}
if err := user.closeStore(); err != nil {
result = multierror.Append(result, err)
}
}
if err := b.config.ClearData(); err != nil {
result = multierror.Append(result, err)
}
return result.ErrorOrNil()
}
// DeleteUser deletes user completely; it logs user out from the API, stops any
// active connection, deletes from credentials store and removes from the Bridge struct.
func (b *Bridge) DeleteUser(userID string, clearStore bool) error {
b.lock.Lock()
defer b.lock.Unlock()
log := log.WithField("user", userID)
for idx, user := range b.users {
if user.ID() == userID {
if err := user.Logout(); err != nil {
log.WithError(err).Error("Cannot logout user")
// We can try to continue to remove the user.
// Token will still be valid, but will expire eventually.
}
if err := user.closeStore(); err != nil {
log.WithError(err).Error("Failed to close user store")
}
if clearStore {
// Clear cache after closing connections (done in logout).
if err := user.clearStore(); err != nil {
log.WithError(err).Error("Failed to clear user")
}
}
if err := b.credStorer.Delete(userID); err != nil {
log.WithError(err).Error("Cannot remove user")
return err
}
b.users = append(b.users[:idx], b.users[idx+1:]...)
return nil
}
}
return errors.New("user " + userID + " not found")
}
// ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
apiClient := b.pmapiClientFactory("bug_reporter")
title := "[Bridge] Bug"
err := apiClient.ReportBugWithEmailClient(
osType,
osVersion,
title,
description,
accountName,
address,
emailClient,
)
if err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
}
// SendMetric sends a metric. We don't want to return any errors, only log them.
func (b *Bridge) SendMetric(m m.Metric) {
apiClient := b.pmapiClientFactory("metric_reporter")
cat, act, lab := m.Get()
err := apiClient.SendSimpleMetric(string(cat), string(act), string(lab))
if err != nil {
log.Error("Sending metric failed: ", err)
}
log.WithFields(logrus.Fields{
"cat": cat,
"act": act,
"lab": lab,
}).Debug("Metric successfully sent")
}
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
func (b *Bridge) GetCurrentClient() string {
res := b.userAgentClientName
if b.userAgentClientVersion != "" {
res = res + " " + b.userAgentClientVersion
}
return res
}
// SetCurrentClient updates client info (e.g. Thunderbird) and sets the user agent
// on pmapi. By default no client is used, IMAP has to detect it on first login.
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
b.userAgentClientName = clientName
b.userAgentClientVersion = clientVersion
b.updateCurrentUserAgent()
}
// SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
// `runtime.GOOS`, but this can be overridden in case of better detection.
func (b *Bridge) SetCurrentOS(os string) {
b.userAgentOS = os
b.updateCurrentUserAgent()
}
// 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")
}
return b.idleUpdates
}
// AllowDoH instructs bridge to use DoH to access an API proxy if necessary.
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func AllowDoH() {
pmapi.GlobalAllowDoH()
}
// DisallowDoH instructs bridge to not use DoH to access an API proxy if necessary.
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func DisallowDoH() {
pmapi.GlobalDisallowDoH()
}
func (b *Bridge) updateCurrentUserAgent() {
UpdateCurrentUserAgent(b.version, b.userAgentOS, b.userAgentClientName, b.userAgentClientVersion)
}
// hasUser returns whether the bridge currently has a user with ID `id`.
func (b *Bridge) hasUser(id string) (user *User, ok bool) {
for _, u := range b.users {
if u.ID() == id {
user, ok = u, true
return
}
}
return
}
// "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

@ -0,0 +1,256 @@
// 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 (
"io/ioutil"
"os"
"testing"
"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/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
if os.Getenv("VERBOSITY") == "trace" {
logrus.SetLevel(logrus.TraceLevel)
}
os.Exit(m.Run())
}
var (
testAuth = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "tok",
KeySalt: "", // No salting in tests.
}
testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "reftok",
KeySalt: "", // No salting in tests.
}
testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals]
UserID: "user",
Name: "username",
Emails: "user@pm.me",
APIToken: "token",
MailboxPassword: "pass",
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
IsHidden: false,
IsCombinedAddressMode: true,
}
testCredentialsSplit = &credentials.Credentials{ //nolint[gochecknoglobals]
UserID: "users",
Name: "usersname",
Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me",
APIToken: "token",
MailboxPassword: "pass",
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
IsHidden: false,
IsCombinedAddressMode: false,
}
testCredentialsDisconnected = &credentials.Credentials{ //nolint[gochecknoglobals]
UserID: "user",
Name: "username",
Emails: "user@pm.me",
APIToken: "",
MailboxPassword: "",
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
IsHidden: false,
IsCombinedAddressMode: true,
}
testPMAPIUser = &pmapi.User{ //nolint[gochecknoglobals]
ID: "user",
Name: "username",
}
testPMAPIAddress = &pmapi.Address{ //nolint[gochecknoglobals]
ID: "testAddressID",
Type: pmapi.OriginalAddress,
Email: "user@pm.me",
Receive: pmapi.CanReceive,
}
testPMAPIAddresses = []*pmapi.Address{ //nolint[gochecknoglobals]
{ID: "usersAddress1ID", Email: "users@pm.me", Receive: pmapi.CanReceive, Type: pmapi.OriginalAddress},
{ID: "usersAddress2ID", Email: "anotheruser@pm.me", Receive: pmapi.CanReceive, Type: pmapi.AliasAddress},
{ID: "usersAddress3ID", Email: "alsouser@pm.me", Receive: pmapi.CanReceive, Type: pmapi.AliasAddress},
}
testPMAPIEvent = &pmapi.Event{ // nolint[gochecknoglobals]
EventID: "ACXDmTaBub14w==",
}
)
func waitForEvents() {
// Wait for goroutine to add listener.
// E.g. calling login to invoke firstsync event. Functions can end sooner than
// goroutines call the listener mock. We need to wait a little bit before the end of
// the test to capture all event calls. This allows us to detect whether there were
// missing calls, or perhaps whether something was called too many times.
time.Sleep(100 * time.Millisecond)
}
type mocks struct {
t *testing.T
ctrl *gomock.Controller
config *bridgemocks.MockConfiger
PanicHandler *bridgemocks.MockPanicHandler
prefProvider *bridgemocks.MockPreferenceProvider
pmapiClient *bridgemocks.MockPMAPIProvider
credentialsStore *bridgemocks.MockCredentialsStorer
eventListener *MockListener
storeCache *store.Cache
}
func initMocks(t *testing.T) mocks {
mockCtrl := gomock.NewController(t)
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache")
m := mocks{
t: t,
ctrl: mockCtrl,
config: bridgemocks.NewMockConfiger(mockCtrl),
PanicHandler: bridgemocks.NewMockPanicHandler(mockCtrl),
pmapiClient: bridgemocks.NewMockPMAPIProvider(mockCtrl),
prefProvider: bridgemocks.NewMockPreferenceProvider(mockCtrl),
credentialsStore: bridgemocks.NewMockCredentialsStorer(mockCtrl),
eventListener: NewMockListener(mockCtrl),
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.
m.PanicHandler.EXPECT().HandlePanic().AnyTimes()
return m
}
func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge {
// Init for user.
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(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.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.
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
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)
m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil)
return testNewBridge(t, m)
}
func testNewBridge(t *testing.T, m mocks) *Bridge {
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache")
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()
return bridge
}
func cleanUpBridgeUserData(b *Bridge) {
for _, user := range b.users {
_ = user.clearStore()
}
}
func TestClearData(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "alsouser@pm.me")
m.pmapiClient.EXPECT().Logout()
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Logout()
m.credentialsStore.EXPECT().Logout("users").Return(nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil)
m.config.EXPECT().ClearData().Return(nil)
require.NoError(t, bridge.ClearData())
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

@ -0,0 +1,23 @@
// 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
// Host settings.
const (
Host = "127.0.0.1"
)

View File

@ -0,0 +1,137 @@
// 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 credentials implements our struct stored in keychain.
// Store struct is kind of like a database client.
// Credentials struct is kind of like one record from the database.
package credentials
import (
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/sirupsen/logrus"
)
const sep = "\x00"
var (
log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
ErrWrongFormat = errors.New("backend/creds: malformed password")
)
type Credentials struct {
UserID, // Do not marshal; used as a key.
Name,
Emails,
APIToken,
MailboxPassword,
BridgePassword,
Version string
Timestamp int64
IsHidden, // Deprecated.
IsCombinedAddressMode bool
}
func (s *Credentials) Marshal() string {
items := []string{
s.Name, // 0
s.Emails, // 1
s.APIToken, // 2
s.MailboxPassword, // 3
s.BridgePassword, // 4
s.Version, // 5
"", // 6
"", // 7
"", // 8
}
items[6] = fmt.Sprint(s.Timestamp)
if s.IsHidden {
items[7] = "1"
}
if s.IsCombinedAddressMode {
items[8] = "1"
}
str := strings.Join(items, sep)
return base64.StdEncoding.EncodeToString([]byte(str))
}
func (s *Credentials) Unmarshal(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
items := strings.Split(string(b), sep)
if len(items) != 9 {
return ErrWrongFormat
}
s.Name = items[0]
s.Emails = items[1]
s.APIToken = items[2]
s.MailboxPassword = items[3]
s.BridgePassword = items[4]
s.Version = items[5]
if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil {
s.Timestamp = 0
}
if s.IsHidden = false; items[7] == "1" {
s.IsHidden = true
}
if s.IsCombinedAddressMode = false; items[8] == "1" {
s.IsCombinedAddressMode = true
}
return nil
}
func (s *Credentials) SetEmailList(list []string) {
s.Emails = strings.Join(list, ";")
}
func (s *Credentials) EmailList() []string {
return strings.Split(s.Emails, ";")
}
func (s *Credentials) CheckPassword(password string) error {
if subtle.ConstantTimeCompare([]byte(s.BridgePassword), []byte(password)) != 1 {
log.WithFields(logrus.Fields{
"userID": s.UserID,
}).Debug("Incorrect bridge password")
return fmt.Errorf("backend/credentials: incorrect password")
}
return nil
}
func (s *Credentials) Logout() {
s.APIToken = ""
s.MailboxPassword = ""
}
func (s *Credentials) IsConnected() bool {
return s.APIToken != "" && s.MailboxPassword != ""
}

View File

@ -0,0 +1,39 @@
// 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 credentials
import (
"crypto/rand"
"encoding/base64"
"io"
)
const keySize = 16
// generateKey generates a new random key.
func generateKey() []byte {
key := make([]byte, keySize)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
panic(err)
}
return key
}
func generatePassword() string {
return base64.RawURLEncoding.EncodeToString(generateKey())
}

View File

@ -0,0 +1,316 @@
// 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 credentials
import (
"errors"
"fmt"
"sort"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/sirupsen/logrus"
)
var storeLocker = sync.RWMutex{} //nolint[gochecknoglobals]
// Store is an encrypted credentials store.
type Store struct {
secrets *keychain.Access
}
// NewStore creates a new encrypted credentials store.
func NewStore() (*Store, error) {
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) {
storeLocker.Lock()
defer storeLocker.Unlock()
log.WithFields(logrus.Fields{
"user": userID,
"username": userName,
"emails": emails,
}).Trace("Adding new credentials")
if err = s.checkKeychain(); err != nil {
return
}
creds = &Credentials{
UserID: userID,
Name: userName,
APIToken: apiToken,
MailboxPassword: mailboxPassword,
IsHidden: false,
}
creds.SetEmailList(emails)
var has bool
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")
currentCredentials, err := s.get(userID)
if err != nil {
return nil, err
}
creds.BridgePassword = currentCredentials.BridgePassword
creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode
creds.Timestamp = currentCredentials.Timestamp
} else {
log.Info("Generating credentials for new user")
creds.BridgePassword = generatePassword()
creds.IsCombinedAddressMode = true
creds.Timestamp = time.Now().Unix()
}
if err = s.saveCredentials(creds); err != nil {
return
}
return creds, err
}
func (s *Store) SwitchAddressMode(userID string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.IsCombinedAddressMode = !credentials.IsCombinedAddressMode
credentials.BridgePassword = generatePassword()
return s.saveCredentials(credentials)
}
func (s *Store) UpdateEmails(userID string, emails []string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.SetEmailList(emails)
return s.saveCredentials(credentials)
}
func (s *Store) UpdateToken(userID, apiToken string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.APIToken = apiToken
return s.saveCredentials(credentials)
}
func (s *Store) Logout(userID string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.Logout()
return s.saveCredentials(credentials)
}
// List returns a list of usernames that have credentials stored.
func (s *Store) List() (userIDs []string, err error) {
storeLocker.RLock()
defer storeLocker.RUnlock()
log.Trace("Listing credentials in credentials store")
if err = s.checkKeychain(); err != nil {
return
}
var allUserIDs []string
if allUserIDs, err = s.secrets.List(); err != nil {
log.WithError(err).Error("Could not list credentials")
return
}
credentialList := []*Credentials{}
for _, userID := range allUserIDs {
creds, getErr := s.get(userID)
if getErr != nil {
log.WithField("userID", userID).WithError(getErr).Warn("Failed to get credentials")
continue
}
if creds.Timestamp == 0 {
continue
}
credentialList = append(credentialList, creds)
}
sort.Slice(credentialList, func(i, j int) bool {
return credentialList[i].Timestamp < credentialList[j].Timestamp
})
for _, credentials := range credentialList {
userIDs = append(userIDs, credentials.UserID)
}
return userIDs, err
}
func (s *Store) GetAndCheckPassword(userID, password string) (creds *Credentials, err error) {
storeLocker.RLock()
defer storeLocker.RUnlock()
log.WithFields(logrus.Fields{
"userID": userID,
}).Debug("Checking bridge password")
credentials, err := s.Get(userID)
if err != nil {
return nil, err
}
if err := credentials.CheckPassword(password); err != nil {
log.WithFields(logrus.Fields{
"userID": userID,
"err": err,
}).Debug("Incorrect bridge password")
return nil, err
}
return credentials, nil
}
func (s *Store) Get(userID string) (creds *Credentials, err error) {
storeLocker.RLock()
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)
}
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) {
log := log.WithField("user", userID)
if err = s.checkKeychain(); err != nil {
return
}
secret, err := s.secrets.Get(userID)
if err != nil {
log.WithError(err).Error("Could not get credentials from native keychain")
return
}
credentials := &Credentials{UserID: userID}
if err = credentials.Unmarshal(secret); err != nil {
err = fmt.Errorf("backend/credentials: malformed secret: %v", err)
_ = s.secrets.Delete(userID)
log.WithError(err).Error("Could not unmarshal secret")
return
}
return credentials, nil
}
// saveCredentials encrypts and saves password to the keychain store.
func (s *Store) saveCredentials(credentials *Credentials) (err error) {
if err = s.checkKeychain(); err != nil {
return
}
credentials.Version = keychain.KeychainVersion
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.
func (s *Store) Delete(userID string) (err error) {
storeLocker.Lock()
defer storeLocker.Unlock()
if err = s.checkKeychain(); err != nil {
return
}
return s.secrets.Delete(userID)
}

View File

@ -0,0 +1,297 @@
// 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 credentials
import (
"bytes"
"encoding/base64"
"encoding/gob"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testSep = "\n"
const secretFormat = "%v" + testSep + // UserID,
"%v" + testSep + // Name,
"%v" + testSep + // Emails,
"%v" + testSep + // APIToken,
"%v" + testSep + // Mailbox,
"%v" + testSep + // BridgePassword,
"%v" + testSep + // Version string
"%v" + testSep + // Timestamp,
"%v" + testSep + // IsHidden,
"%v" // IsCombinedAddressMode
// the best would be to run this test on mac, win, and linux separately
type testCredentials struct {
UserID,
Name,
Emails,
APIToken,
Mailbox,
BridgePassword,
Version string
Timestamp int64
IsHidden,
IsCombinedAddressMode bool
}
func init() { //nolint[gochecknoinits]
gob.Register(testCredentials{})
}
func (s *testCredentials) MarshalGob() string {
buf := bytes.Buffer{}
enc := gob.NewEncoder(&buf)
if err := enc.Encode(s); err != nil {
return ""
}
fmt.Printf("MarshalGob: %#v\n", buf.String())
return base64.StdEncoding.EncodeToString(buf.Bytes())
}
func (s *testCredentials) Clear() {
s.UserID = ""
s.Name = ""
s.Emails = ""
s.APIToken = ""
s.Mailbox = ""
s.BridgePassword = ""
s.Version = ""
s.Timestamp = 0
s.IsHidden = false
s.IsCombinedAddressMode = false
}
func (s *testCredentials) UnmarshalGob(secret string) error {
s.Clear()
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
fmt.Println("decode base64", b)
return err
}
buf := bytes.NewBuffer(b)
dec := gob.NewDecoder(buf)
if err = dec.Decode(s); err != nil {
fmt.Println("decode gob", b, buf.Bytes())
return err
}
return nil
}
func (s *testCredentials) ToJSON() string {
if b, err := json.Marshal(s); err == nil {
fmt.Printf("MarshalJSON: %#v\n", string(b))
return base64.StdEncoding.EncodeToString(b)
}
return ""
}
func (s *testCredentials) FromJSON(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
if err = json.Unmarshal(b, s); err == nil {
return nil
}
return err
}
func (s *testCredentials) MarshalFmt() string {
buf := bytes.Buffer{}
fmt.Fprintf(
&buf, secretFormat,
s.UserID,
s.Name,
s.Emails,
s.APIToken,
s.Mailbox,
s.BridgePassword,
s.Version,
s.Timestamp,
s.IsHidden,
s.IsCombinedAddressMode,
)
fmt.Printf("MarshalFmt: %#v\n", buf.String())
return base64.StdEncoding.EncodeToString(buf.Bytes())
}
func (s *testCredentials) UnmarshalFmt(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
buf := bytes.NewBuffer(b)
fmt.Println("decode fmt", b, buf.Bytes())
_, err = fmt.Fscanf(
buf, secretFormat,
&s.UserID,
&s.Name,
&s.Emails,
&s.APIToken,
&s.Mailbox,
&s.BridgePassword,
&s.Version,
&s.Timestamp,
&s.IsHidden,
&s.IsCombinedAddressMode,
)
if err != nil {
return err
}
return nil
}
func (s *testCredentials) MarshalStrings() string { // this is the most space efficient
items := []string{
s.UserID, // 0
s.Name, // 1
s.Emails, // 2
s.APIToken, // 3
s.Mailbox, // 4
s.BridgePassword, // 5
s.Version, // 6
}
items = append(items, fmt.Sprint(s.Timestamp)) // 7
if s.IsHidden { // 8
items = append(items, "1")
} else {
items = append(items, "")
}
if s.IsCombinedAddressMode { // 9
items = append(items, "1")
} else {
items = append(items, "")
}
str := strings.Join(items, sep)
fmt.Printf("MarshalJoin: %#v\n", str)
return base64.StdEncoding.EncodeToString([]byte(str))
}
func (s *testCredentials) UnmarshalStrings(secret string) error {
b, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
items := strings.Split(string(b), sep)
if len(items) != 10 {
return ErrWrongFormat
}
s.UserID = items[0]
s.Name = items[1]
s.Emails = items[2]
s.APIToken = items[3]
s.Mailbox = items[4]
s.BridgePassword = items[5]
s.Version = items[6]
if _, err = fmt.Sscanf(items[7], "%d", &s.Timestamp); err != nil {
s.Timestamp = 0
}
if s.IsHidden = false; items[8] == "1" {
s.IsHidden = true
}
if s.IsCombinedAddressMode = false; items[9] == "1" {
s.IsCombinedAddressMode = true
}
return nil
}
func (s *testCredentials) IsSame(rhs *testCredentials) bool {
return s.Name == rhs.Name &&
s.Emails == rhs.Emails &&
s.APIToken == rhs.APIToken &&
s.Mailbox == rhs.Mailbox &&
s.BridgePassword == rhs.BridgePassword &&
s.Version == rhs.Version &&
s.Timestamp == rhs.Timestamp &&
s.IsHidden == rhs.IsHidden &&
s.IsCombinedAddressMode == rhs.IsCombinedAddressMode
}
func TestMarshalFormats(t *testing.T) {
input := testCredentials{UserID: "007", Emails: "ja@pm.me;jakub@cu.th", Timestamp: 152469263742, IsHidden: true}
fmt.Printf("input %#v\n", input)
secretStrings := input.MarshalStrings()
fmt.Printf("secretStrings %#v %d\n", secretStrings, len(secretStrings))
secretGob := input.MarshalGob()
fmt.Printf("secretGob %#v %d\n", secretGob, len(secretGob))
secretJSON := input.ToJSON()
fmt.Printf("secretJSON %#v %d\n", secretJSON, len(secretJSON))
secretFmt := input.MarshalFmt()
fmt.Printf("secretFmt %#v %d\n", secretFmt, len(secretFmt))
output := testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalStrings(secretStrings))
fmt.Printf("strings out %#v \n", output)
require.True(t, input.IsSame(&output), "strings out not same")
output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalGob(secretGob))
fmt.Printf("gob out %#v\n \n", output)
assert.Equal(t, input, output)
output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.FromJSON(secretJSON))
fmt.Printf("json out %#v \n", output)
require.True(t, input.IsSame(&output), "json out not same")
/*
// Simple Fscanf not working!
output = testCredentials{APIToken: "refresh"}
require.NoError(t, output.UnmarshalFmt(secretFmt))
fmt.Printf("fmt out %#v \n", output)
require.True(t, input.IsSame(&output), "fmt out not same")
*/
}
func TestMarshal(t *testing.T) {
input := Credentials{
UserID: "",
Name: "007",
Emails: "ja@pm.me;aj@cus.tom",
APIToken: "sdfdsfsdfsdfsdf",
MailboxPassword: "cdcdcdcd",
BridgePassword: "wew123",
Version: "k11",
Timestamp: 152469263742,
IsHidden: true,
IsCombinedAddressMode: false,
}
fmt.Printf("input %#v\n", input)
secret := input.Marshal()
fmt.Printf("secret %#v %d\n", secret, len(secret))
output := Credentials{APIToken: "refresh"}
require.NoError(t, output.Unmarshal(secret))
fmt.Printf("output %#v\n", 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 Mon Apr 6 08:14:14 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-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

@ -0,0 +1,107 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./listener/listener.go
// Package bridge is a generated GoMock package.
package bridge
import (
reflect "reflect"
time "time"
gomock "github.com/golang/mock/gomock"
)
// MockListener is a mock of Listener interface
type MockListener struct {
ctrl *gomock.Controller
recorder *MockListenerMockRecorder
}
// MockListenerMockRecorder is the mock recorder for MockListener
type MockListenerMockRecorder struct {
mock *MockListener
}
// NewMockListener creates a new mock instance
func NewMockListener(ctrl *gomock.Controller) *MockListener {
mock := &MockListener{ctrl: ctrl}
mock.recorder = &MockListenerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockListener) EXPECT() *MockListenerMockRecorder {
return m.recorder
}
// SetLimit mocks base method
func (m *MockListener) SetLimit(eventName string, limit time.Duration) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetLimit", eventName, limit)
}
// SetLimit indicates an expected call of SetLimit
func (mr *MockListenerMockRecorder) SetLimit(eventName, limit interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockListener)(nil).SetLimit), eventName, limit)
}
// Add mocks base method
func (m *MockListener) Add(eventName string, channel chan<- string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Add", eventName, channel)
}
// Add indicates an expected call of Add
func (mr *MockListenerMockRecorder) Add(eventName, channel interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockListener)(nil).Add), eventName, channel)
}
// Remove mocks base method
func (m *MockListener) Remove(eventName string, channel chan<- string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Remove", eventName, channel)
}
// Remove indicates an expected call of Remove
func (mr *MockListenerMockRecorder) Remove(eventName, channel interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockListener)(nil).Remove), eventName, channel)
}
// Emit mocks base method
func (m *MockListener) Emit(eventName, data string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Emit", eventName, data)
}
// Emit indicates an expected call of Emit
func (mr *MockListenerMockRecorder) Emit(eventName, data interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), eventName, data)
}
// SetBuffer mocks base method
func (m *MockListener) SetBuffer(eventName string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetBuffer", eventName)
}
// SetBuffer indicates an expected call of SetBuffer
func (mr *MockListenerMockRecorder) SetBuffer(eventName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBuffer", reflect.TypeOf((*MockListener)(nil).SetBuffer), eventName)
}
// RetryEmit mocks base method
func (m *MockListener) RetryEmit(eventName string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "RetryEmit", eventName)
}
// RetryEmit indicates an expected call of RetryEmit
func (mr *MockListenerMockRecorder) RetryEmit(eventName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryEmit", reflect.TypeOf((*MockListener)(nil).RetryEmit), eventName)
}

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

@ -0,0 +1,34 @@
// 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 ./release-notes.sh at Mon Apr 6 08:14:14 CEST 2020. DO NOT EDIT.
package bridge
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 rare case of sending the same message multiple times in Outlook
• Fixed bug in macOS update process; available from next update
`

105
internal/bridge/types.go Normal file
View File

@ -0,0 +1,105 @@
// 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 (
"io"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" // mockgen needs this to be given an explicit import name
)
type Configer interface {
ClearData() error
GetDBDir() string
GetIMAPCachePath() string
GetAPIConfig() *pmapi.ClientConfig
}
type PreferenceProvider interface {
Get(key string) string
GetBool(key string) bool
GetInt(key string) int
Set(key string, value string)
}
type PanicHandler interface {
HandlePanic()
}
type PMAPIProviderFactory func(string) PMAPIProvider
type PMAPIProvider interface {
SetAuths(auths chan<- *pmapi.Auth)
Auth(username, password string, info *pmapi.AuthInfo) (*pmapi.Auth, error)
AuthInfo(username string) (*pmapi.AuthInfo, error)
AuthRefresh(token string) (*pmapi.Auth, error)
Unlock(mailboxPassword string) (kr *pmcrypto.KeyRing, err error)
UnlockAddresses(passphrase []byte) error
CurrentUser() (*pmapi.User, error)
UpdateUser() (*pmapi.User, error)
Addresses() pmapi.AddressList
Logout() error
GetEvent(eventID string) (*pmapi.Event, error)
CountMessages(addressID string) ([]*pmapi.MessagesCount, error)
ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Message, int, error)
GetMessage(apiID string) (*pmapi.Message, error)
Import([]*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error)
DeleteMessages(apiIDs []string) error
LabelMessages(apiIDs []string, labelID string) error
UnlabelMessages(apiIDs []string, labelID string) error
MarkMessagesRead(apiIDs []string) error
MarkMessagesUnread(apiIDs []string) error
ListLabels() ([]*pmapi.Label, error)
CreateLabel(label *pmapi.Label) (*pmapi.Label, error)
UpdateLabel(label *pmapi.Label) (*pmapi.Label, error)
DeleteLabel(labelID string) error
EmptyFolder(labelID string, addressID string) error
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
SendSimpleMetric(category, action, label string) error
Auth2FA(twoFactorCode string, auth *pmapi.Auth) (*pmapi.Auth2FA, error)
GetMailSettings() (pmapi.MailSettings, error)
GetContactEmailByEmail(string, int, int) ([]pmapi.ContactEmail, error)
GetContactByID(string) (pmapi.Contact, error)
DecryptAndVerifyCards([]pmapi.Card) ([]pmapi.Card, error)
GetPublicKeysForEmail(string) ([]pmapi.PublicKey, bool, error)
SendMessage(string, *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err error)
CreateDraft(m *pmapi.Message, parent string, action int) (created *pmapi.Message, err error)
CreateAttachment(att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error)
KeyRingForAddressID(string) (kr *pmcrypto.KeyRing)
GetAttachment(id string) (att io.ReadCloser, err error)
}
type CredentialsStorer interface {
List() (userIDs []string, err error)
Add(userID, userName, apiToken, mailboxPassword string, emails []string) (*credentials.Credentials, error)
Get(userID string) (*credentials.Credentials, error)
SwitchAddressMode(userID string) error
UpdateEmails(userID string, emails []string) error
UpdateToken(userID, apiToken string) error
Logout(userID string) error
Delete(userID string) error
}

621
internal/bridge/user.go Normal file
View File

@ -0,0 +1,621 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"fmt"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
logrus "github.com/sirupsen/logrus"
)
// ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from bridge.
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.
type User struct {
log *logrus.Entry
panicHandler PanicHandler
listener listener.Listener
apiClient PMAPIProvider
credStorer CredentialsStorer
imapUpdatesChannel chan interface{}
store *store.Store
storeCache *store.Cache
storePath string
userID string
creds *credentials.Credentials
lock sync.RWMutex
authChannel chan *pmapi.Auth
hasAPIAuth bool
unlockingKeyringLock sync.Mutex
wasKeyringUnlocked bool
}
// newUser creates a new bridge user.
func newUser(
panicHandler PanicHandler,
userID string,
eventListener listener.Listener,
credStorer CredentialsStorer,
apiClient PMAPIProvider,
storeCache *store.Cache,
storeDir string,
) (u *User, err error) {
log := log.WithField("user", userID)
log.Debug("Creating or loading user")
creds, err := credStorer.Get(userID)
if err != nil {
return nil, errors.Wrap(err, "failed to load user credentials")
}
u = &User{
log: log,
panicHandler: panicHandler,
listener: eventListener,
credStorer: credStorer,
apiClient: apiClient,
storeCache: storeCache,
storePath: getUserStorePath(storeDir, userID),
userID: userID,
creds: creds,
}
return
}
// init initialises a bridge 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
// 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
// something in the store changed).
func (u *User) init(idleUpdates chan interface{}, apiClient PMAPIProvider) (err error) {
// 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
// version with the apitoken and mailbox password).
creds, err := u.credStorer.Get(u.userID)
if err != nil {
return errors.Wrap(err, "failed to load user credentials")
}
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.
// Note: we still allow users to set up bridge if the internet is off.
if authErr := u.authorizeIfNecessary(false); authErr != nil {
switch errors.Cause(authErr) {
case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser:
u.log.WithError(authErr).Warn("Could not authorize user")
default:
if logoutErr := u.logout(); logoutErr != nil {
u.log.WithError(logoutErr).Warn("Could not logout user")
}
return errors.Wrap(authErr, "failed to authorize user")
}
}
// Logged-out user keeps store running to access offline data.
// Therefore it is necessary to close it before re-init.
if u.store != nil {
if err := u.store.Close(); err != nil {
log.WithError(err).Error("Not able to close store")
}
u.store = nil
}
store, err := store.New(u.panicHandler, u, u.apiClient, u.listener, u.storePath, u.storeCache)
if err != nil {
return errors.Wrap(err, "failed to create store")
}
u.store = store
// Save the imap updates channel here so it can be set later when imap connects.
u.imapUpdatesChannel = idleUpdates
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.
// If user is not already connected to the api auth channel (for example there was no internet during start),
// it tries to connect it. See `connectToAuthChannel` for more info.
func (u *User) authorizeIfNecessary(emitEvent bool) (err error) {
// If user is connected and has an auth channel, then perfect, nothing to do here.
if u.creds.IsConnected() && u.HasAPIAuth() {
// The keyring unlock is triggered here to resolve state where apiClient
// is authenticated (we have auth token) but it was not possible to download
// and unlock the keys (internet not reachable).
return u.unlockIfNecessary()
}
if !u.creds.IsConnected() {
err = ErrLoggedOutUser
} else if err = u.authorizeAndUnlock(); err != nil {
u.log.WithError(err).Error("Could not authorize and unlock user")
switch errors.Cause(err) {
case pmapi.ErrUpgradeApplication:
u.listener.Emit(events.UpgradeApplicationEvent, "")
case pmapi.ErrAPINotReachable:
u.listener.Emit(events.InternetOffEvent, "")
default:
if errLogout := u.credStorer.Logout(u.userID); errLogout != nil {
u.log.WithField("err", errLogout).Error("Could not log user out from credentials store")
}
}
}
if emitEvent && err != nil &&
errors.Cause(err) != pmapi.ErrUpgradeApplication &&
errors.Cause(err) != pmapi.ErrAPINotReachable {
u.listener.Emit(events.LogoutEvent, u.userID)
}
return err
}
// unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked.
func (u *User) unlockIfNecessary() error {
u.unlockingKeyringLock.Lock()
defer u.unlockingKeyringLock.Unlock()
if u.wasKeyringUnlocked {
return nil
}
if _, err := u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
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
}
// authorizeAndUnlock tries to authorize the user with the API using the the user's APIToken.
// If that succeeds, it tries to unlock the user's keys and addresses.
func (u *User) authorizeAndUnlock() (err error) {
if u.creds.APIToken == "" {
u.log.Warn("Could not connect to API auth channel, have no API token")
return nil
}
auth, err := u.apiClient.AuthRefresh(u.creds.APIToken)
if err != nil {
return errors.Wrap(err, "failed to refresh API auth")
}
u.authChannel <- auth
if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
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
}
// See `connectToAPIClientAuthChannel` for more info.
func (u *User) watchAPIClientAuths() {
for auth := range u.authChannel {
if auth != nil {
newRefreshToken := auth.UID() + ":" + auth.RefreshToken
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")
}
}
}
// 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.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.
func (u *User) clearStore() error {
u.log.Trace("Clearing user store")
if u.store != nil {
if err := u.store.Remove(); err != nil {
return errors.Wrap(err, "failed to remove store")
}
} else {
u.log.Warn("Store is not initialized: cleaning up store files manually")
if err := store.RemoveStore(u.storeCache, u.storePath, u.userID); err != nil {
return errors.Wrap(err, "failed to remove store manually")
}
}
return nil
}
// closeStore just closes the store without deleting it.
func (u *User) closeStore() error {
u.log.Trace("Closing user store")
if u.store != nil {
if err := u.store.Close(); err != nil {
return errors.Wrap(err, "failed to close store")
}
}
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.
// 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.
func (u *User) GetTemporaryPMAPIClient() PMAPIProvider {
return u.apiClient
}
// ID returns the user's userID.
func (u *User) ID() string {
return u.userID
}
// Username returns the user's username as found in the user's credentials.
func (u *User) Username() string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.Name
}
// IsConnected returns whether user is logged in.
func (u *User) IsConnected() bool {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.IsConnected()
}
// IsCombinedAddressMode returns whether user is set in combined or split mode.
// Combined mode is the default mode and is what users typically need.
// Split mode is mostly for outlook as it cannot handle sending e-mails from an
// address other than the primary one.
func (u *User) IsCombinedAddressMode() bool {
if u.store != nil {
return u.store.IsCombinedMode()
}
return u.creds.IsCombinedAddressMode
}
// GetPrimaryAddress returns the user's original address (which is
// not necessarily the same as the primary address, because a primary address
// might be an alias and be in position one).
func (u *User) GetPrimaryAddress() string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.EmailList()[0]
}
// GetStoreAddresses returns all addresses used by the store (so in combined mode,
// that's just the original address, but in split mode, that's all active addresses).
func (u *User) GetStoreAddresses() []string {
u.lock.RLock()
defer u.lock.RUnlock()
if u.IsCombinedAddressMode() {
return u.creds.EmailList()[:1]
}
return u.creds.EmailList()
}
// getStoreAddresses returns a user's used addresses (with the original address in first place).
func (u *User) getStoreAddresses() []string { // nolint[unused]
addrInfo, err := u.store.GetAddressInfo()
if err != nil {
u.log.WithError(err).Error("Failed getting address info from store")
return nil
}
addresses := []string{}
for _, addr := range addrInfo {
addresses = append(addresses, addr.Address)
}
if u.IsCombinedAddressMode() {
return addresses[:1]
}
return addresses
}
// GetAddresses returns list of all addresses.
func (u *User) GetAddresses() []string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.EmailList()
}
// GetAddressID returns the API ID of the given address.
func (u *User) GetAddressID(address string) (id string, err error) {
u.lock.RLock()
defer u.lock.RUnlock()
address = strings.ToLower(address)
if u.store == nil {
err = errors.New("store is not initialised")
return
}
return u.store.GetAddressID(address)
}
// GetBridgePassword returns bridge password. This is not a password of the PM
// account, but generated password for local purposes to not use a PM account
// in the clients (such as Thunderbird).
func (u *User) GetBridgePassword() string {
u.lock.RLock()
defer u.lock.RUnlock()
return u.creds.BridgePassword
}
// CheckBridgeLogin checks whether the user is logged in and the bridge
// password is correct.
func (u *User) CheckBridgeLogin(password string) error {
if isApplicationOutdated {
u.listener.Emit(events.UpgradeApplicationEvent, "")
return pmapi.ErrUpgradeApplication
}
u.lock.RLock()
defer u.lock.RUnlock()
// True here because users should be notified by popup of auth failure.
if err := u.authorizeIfNecessary(true); err != nil {
u.log.WithError(err).Error("Failed to authorize user")
return err
}
return u.creds.CheckPassword(password)
}
// UpdateUser updates user details from API and saves to the credentials.
func (u *User) UpdateUser() error {
u.lock.Lock()
defer u.lock.Unlock()
if err := u.authorizeIfNecessary(true); err != nil {
return errors.Wrap(err, "cannot update user")
}
_, err := u.apiClient.UpdateUser()
if err != nil {
return err
}
if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
return err
}
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 {
return err
}
u.refreshFromCredentials()
return nil
}
// SwitchAddressMode changes mode from combined to split and vice versa. The mode to switch to is determined by the
// state of the user's credentials in the credentials store. See `IsCombinedAddressMode` for more details.
func (u *User) SwitchAddressMode() (err error) {
u.log.Trace("Switching user address mode")
u.lock.Lock()
defer u.lock.Unlock()
u.closeAllConnections()
if u.store == nil {
err = errors.New("store is not initialised")
return
}
newAddressModeState := !u.IsCombinedAddressMode()
if err = u.store.UseCombinedMode(newAddressModeState); err != nil {
u.log.WithError(err).Error("Could not switch store address mode")
return
}
if u.creds.IsCombinedAddressMode != newAddressModeState {
if err = u.credStorer.SwitchAddressMode(u.userID); err != nil {
u.log.WithError(err).Error("Could not switch credentials store address mode")
return
}
}
u.refreshFromCredentials()
return err
}
// logout is the same as Logout, but for internal purposes (logged out from
// the server) which emits LogoutEvent to notify other parts of the Bridge.
func (u *User) logout() error {
u.lock.Lock()
wasConnected := u.creds.IsConnected()
u.lock.Unlock()
err := u.Logout()
if wasConnected {
u.listener.Emit(events.LogoutEvent, u.userID)
u.listener.Emit(events.UserRefreshEvent, u.userID)
}
return err
}
// Logout logs out the user from pmapi, the credentials store, the mail store, and tries to remove as much
// sensitive data as possible.
func (u *User) Logout() (err error) {
u.lock.Lock()
defer u.lock.Unlock()
u.log.Debug("Logging out user")
if !u.creds.IsConnected() {
return
}
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 {
u.log.WithError(err).Warn("Could not log user out from credentials store")
if err = u.credStorer.Delete(u.userID); err != nil {
u.log.WithError(err).Error("Could not delete user from credentials store")
}
}
u.refreshFromCredentials()
// Do not close whole store, just event loop. Some information might be needed offline (e.g. addressID)
u.closeEventLoop()
u.closeAllConnections()
runtime.GC()
return err
}
func (u *User) refreshFromCredentials() {
if credentials, err := u.credStorer.Get(u.userID); err != nil {
log.Error("Cannot update credentials: ", err)
} else {
u.creds = credentials
}
}
func (u *User) closeEventLoop() {
if u.store == nil {
return
}
u.store.CloseEventLoop()
}
// closeAllConnections calls CloseConnection for all users addresses.
func (u *User) closeAllConnections() {
for _, address := range u.creds.EmailList() {
u.CloseConnection(address)
}
if u.store != nil {
u.store.SetIMAPUpdateChannel(nil)
}
}
// CloseConnection emits closeConnection event on `address` which should close all active connection.
func (u *User) CloseConnection(address string) {
u.listener.Emit(events.CloseConnectionEvent, address)
}
func (u *User) GetStore() *store.Store {
return u.store
}
func (u *User) HasAPIAuth() bool {
return u.hasAPIAuth
}

View File

@ -0,0 +1,209 @@
// 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/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestUpdateUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().UpdateUser().Return(nil, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
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().Get("user").Return(testCredentials, nil)
assert.NoError(t, user.UpdateUser())
waitForEvents()
}
func TestUserSwitchAddressMode(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
assert.True(t, user.store.IsCombinedMode())
assert.True(t, user.creds.IsCombinedAddressMode)
assert.True(t, user.IsCombinedAddressMode())
waitForEvents()
gomock.InOrder(
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
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.credentialsStore.EXPECT().SwitchAddressMode("user").Return(nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsSplit, nil),
)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
assert.NoError(t, user.SwitchAddressMode())
assert.False(t, user.store.IsCombinedMode())
assert.False(t, user.creds.IsCombinedAddressMode)
assert.False(t, user.IsCombinedAddressMode())
gomock.InOrder(
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me"),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me"),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "alsouser@pm.me"),
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.credentialsStore.EXPECT().SwitchAddressMode("user").Return(nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
assert.NoError(t, user.SwitchAddressMode())
assert.True(t, user.store.IsCombinedMode())
assert.True(t, user.creds.IsCombinedAddressMode)
assert.True(t, user.IsCombinedAddressMode())
waitForEvents()
}
func TestLogoutUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := user.Logout()
waitForEvents()
assert.NoError(t, err)
}
func TestLogoutUserFailsLogout(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed"))
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 := user.Logout()
waitForEvents()
assert.NoError(t, err)
}
func TestCheckBridgeLogin(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
err := user.CheckBridgeLogin(testCredentials.BridgePassword)
waitForEvents()
assert.NoError(t, err)
}
func TestCheckBridgeLoginUpgradeApplication(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
isApplicationOutdated = true
err := user.CheckBridgeLogin("any-pass")
waitForEvents()
isApplicationOutdated = false
assert.Equal(t, pmapi.ErrUpgradeApplication, err)
}
func TestCheckBridgeLoginLoggedOut(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
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())
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
_ = user.init(nil, m.pmapiClient)
defer cleanUpUserData(user)
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
err := user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword)
waitForEvents()
assert.Equal(t, "bridge account is logged out, use bridge to login again", err.Error())
}
func TestCheckBridgeLoginBadPassword(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUser(m)
defer cleanUpUserData(user)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
err := user.CheckBridgeLogin("wrong!")
waitForEvents()
assert.Equal(t, "backend/credentials: incorrect password", err.Error())
}

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

@ -0,0 +1,41 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"fmt"
"runtime"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
// UpdateCurrentUserAgent updates user agent on pmapi so each request has this
// information in headers for statistic purposes.
func UpdateCurrentUserAgent(bridgeVersion, os, clientName, clientVersion string) {
if os == "" {
os = runtime.GOOS
}
mailClient := "unknown client"
if clientName != "" {
mailClient = clientName
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)
}

53
internal/events/events.go Normal file
View File

@ -0,0 +1,53 @@
// 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 events provides names of events used by the event listener in bridge.
package events
import (
"time"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
// Constants of events used by the event listener in bridge.
const (
ErrorEvent = "error"
CloseConnectionEvent = "closeConnection"
LogoutEvent = "logout"
AddressChangedEvent = "addressChanged"
AddressChangedLogoutEvent = "addressChangedLogout"
UserRefreshEvent = "userRefresh"
RestartBridgeEvent = "restartBridge"
InternetOffEvent = "internetOff"
InternetOnEvent = "internetOn"
SecondInstanceEvent = "secondInstance"
OutgoingNoEncEvent = "outgoingNoEncryption"
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
UpgradeApplicationEvent = "upgradeApplication"
TLSCertIssue = "tlsCertPinningIssue"
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
LogoutEventTimeout = 3 * time.Minute
)
// SetupEvents specific to event type and data.
func SetupEvents(listener listener.Listener) {
listener.SetLimit(LogoutEvent, LogoutEventTimeout)
listener.SetBuffer(TLSCertIssue)
listener.SetBuffer(ErrorEvent)
}

View File

@ -0,0 +1,108 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build darwin
package autoconfig
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
mobileconfig "github.com/ProtonMail/go-apple-mobileconfig"
)
func init() { //nolint[gochecknoinit]
available = append(available, &appleMail{})
}
type appleMail struct{}
func (c *appleMail) Name() string {
return "Apple Mail"
}
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.BridgeUser, addressIndex int) error { //nolint[funlen]
var addresses string
var displayName string
if user.IsCombinedAddressMode() {
displayName = user.GetPrimaryAddress()
addresses = strings.Join(user.GetAddresses(), ",")
} else {
for idx, address := range user.GetAddresses() {
if idx == addressIndex {
displayName = address
break
}
}
addresses = displayName
}
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
mc := &mobileconfig.Config{
EmailAddress: addresses,
DisplayName: displayName,
Identifier: "protonmail " + displayName + timestamp,
Imap: &mobileconfig.Imap{
Hostname: bridge.Host,
Port: imapPort,
Tls: imapSSL,
Username: displayName,
Password: user.GetBridgePassword(),
},
Smtp: &mobileconfig.Smtp{
Hostname: bridge.Host,
Port: smtpPort,
Tls: smtpSSL,
Username: displayName,
},
}
dir, err := ioutil.TempDir("", "protonmail-autoconfig")
if err != nil {
return err
}
// Make sure the temporary file is deleted.
go (func() {
<-time.After(10 * time.Minute)
_ = os.RemoveAll(dir)
})()
// Make sure the file is only readable for the current user.
f, err := os.OpenFile(filepath.Join(dir, "protonmail.mobileconfig"), os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return err
}
if err := mc.WriteTo(f); err != nil {
_ = f.Close()
return err
}
_ = f.Close()
return exec.Command("open", f.Name()).Run() // nolint[gosec]
}

View File

@ -0,0 +1,33 @@
// 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 autoconfig provides automatic config of IMAP and SMTP.
// For now only for Apple Mail.
package autoconfig
import "github.com/ProtonMail/proton-bridge/internal/frontend/types"
type AutoConfig interface {
Name() string
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.BridgeUser, addressIndex int) error
}
var available []AutoConfig //nolint[gochecknoglobals]
func Available() []AutoConfig {
return available
}

View File

@ -0,0 +1,100 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
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.bridge.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.bridge.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.BridgeUser {
user := f.getUserByIndexOrName("")
if user != nil {
return user
}
numberOfAccounts := len(f.bridge.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.BridgeUser {
users := f.bridge.GetUsers()
numberOfAccounts := len(users)
if numberOfAccounts == 0 {
return nil
}
if numberOfAccounts == 1 {
return users[0]
}
if index, err := strconv.Atoi(arg); err == nil {
if index < 0 || index >= numberOfAccounts {
return nil
}
return users[index]
}
for _, user := range users {
if user.Username() == arg {
return user
}
}
return nil
}

View File

@ -0,0 +1,219 @@
// 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 cli
import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) listAccounts(c *ishell.Context) {
spacing := "%-2d: %-20s (%-15s, %-15s)\n"
f.Printf(bold(strings.Replace(spacing, "d", "s", -1)), "#", "account", "status", "address mode")
for idx, user := range f.bridge.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) showAccountInfo(c *ishell.Context) {
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
if !user.IsConnected() {
f.Printf("Please login to %s to get email client configuration.\n", bold(user.Username()))
return
}
if user.IsCombinedAddressMode() {
f.showAccountAddressInfo(user, user.GetPrimaryAddress())
} else {
for _, address := range user.GetAddresses() {
f.showAccountAddressInfo(user, address)
}
}
}
func (f *frontendCLI) showAccountAddressInfo(user types.BridgeUser, address string) {
smtpSecurity := "STARTTLS"
if f.preferences.GetBool(preferences.SMTPSSLKey) {
smtpSecurity = "SSL"
}
f.Println(bold("Configuration for " + address))
f.Printf("IMAP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
bridge.Host,
f.preferences.GetInt(preferences.IMAPPortKey),
address,
user.GetBridgePassword(),
"STARTTLS",
)
f.Println("")
f.Printf("SMTP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
bridge.Host,
f.preferences.GetInt(preferences.SMTPPortKey),
address,
user.GetBridgePassword(),
smtpSecurity,
)
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.bridge.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.bridge.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.bridge.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.bridge.GetUsers() {
if err := f.bridge.DeleteUser(user.ID(), false); err != nil {
f.printAndLogError("Cannot delete account ", user.Username(), ": ", err)
}
}
c.Println("Keychain cleared")
}
func (f *frontendCLI) changeMode(c *ishell.Context) {
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
newMode := "combined mode"
if user.IsCombinedAddressMode() {
newMode = "split mode"
}
if !f.yesNoQuestion("Are you sure you want to change the mode for account " + bold(user.Username()) + " to " + bold(newMode)) {
return
}
if err := user.SwitchAddressMode(); err != nil {
f.printAndLogError("Cannot switch address mode:", err)
}
f.Printf("Address mode for account %s changed to %s\n", user.Username(), newMode)
}

View File

@ -0,0 +1,264 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package cli provides CLI interface of the Bridge.
package cli
import (
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/abiosoft/ishell"
)
var (
log = config.GetLogEntry("frontend/cli") //nolint[gochecknoglobals]
)
type frontendCLI struct {
*ishell.Shell
config *config.Config
preferences *config.Preferences
eventListener listener.Listener
updates types.Updater
bridge types.Bridger
appRestart bool
}
// New returns a new CLI frontend configured with the given options.
func New( //nolint[funlen]
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
eventListener listener.Listener,
updates types.Updater,
bridge types.Bridger,
) *frontendCLI { //nolint[golint]
fe := &frontendCLI{
Shell: ishell.New(),
config: config,
preferences: preferences,
eventListener: eventListener,
updates: updates,
bridge: bridge,
appRestart: false,
}
// Clear commands.
clearCmd := &ishell.Cmd{Name: "clear",
Help: "remove stored accounts and preferences. (alias: cl)",
Aliases: []string{"cl"},
}
clearCmd.AddCmd(&ishell.Cmd{Name: "cache",
Help: "remove stored preferences for accounts (aliases: c, prefs, preferences)",
Aliases: []string{"c", "prefs", "preferences"},
Func: fe.deleteCache,
})
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: k, keychain)",
Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts,
})
fe.AddCmd(clearCmd)
// Change commands.
changeCmd := &ishell.Cmd{Name: "change",
Help: "change server or account settings (aliases: ch, switch)",
Aliases: []string{"ch", "switch"},
}
changeCmd.AddCmd(&ishell.Cmd{Name: "mode",
Help: "switch between combined addresses and split addresses mode for account. Use index or account name as parameter. (alias: m)",
Aliases: []string{"m"},
Func: fe.changeMode,
Completer: fe.completeUsernames,
})
changeCmd.AddCmd(&ishell.Cmd{Name: "port",
Help: "change port numbers of IMAP and SMTP servers. (alias: p)",
Aliases: []string{"p"},
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",
Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)",
Aliases: []string{"ssl", "starttls"},
Func: fe.changeSMTPSecurity,
})
fe.AddCmd(changeCmd)
// Check commands.
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",
Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"},
Func: fe.checkInternetConnection,
})
fe.AddCmd(checkCmd)
// Print info commands.
fe.AddCmd(&ishell.Cmd{Name: "log-dir",
Help: "print path to directory with logs. (aliases: log, logs)",
Aliases: []string{"log", "logs"},
Func: fe.printLogDir,
})
fe.AddCmd(&ishell.Cmd{Name: "manual",
Help: "print URL with instructions. (alias: man)",
Aliases: []string{"man"},
Func: fe.printManual,
})
fe.AddCmd(&ishell.Cmd{Name: "release-notes",
Help: "print release notes. (aliases: notes, fixed-bugs, bugs, ver, version)",
Aliases: []string{"notes", "fixed-bugs", "bugs", "ver", "version"},
Func: fe.printLocalReleaseNotes,
})
fe.AddCmd(&ishell.Cmd{Name: "credits",
Help: "print used resources.",
Func: fe.printCredits,
})
// Account commands.
fe.AddCmd(&ishell.Cmd{Name: "list",
Help: "print the list of accounts. (aliases: l, ls)",
Func: fe.noAccountWrapper(fe.listAccounts),
Aliases: []string{"l", "ls"},
})
fe.AddCmd(&ishell.Cmd{Name: "info",
Help: "print the configuration for account. Use index or account name as parameter. (alias: i)",
Func: fe.noAccountWrapper(fe.showAccountInfo),
Completer: fe.completeUsernames,
Aliases: []string{"i"},
})
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,
})
// System commands.
fe.AddCmd(&ishell.Cmd{Name: "restart",
Help: "restart the bridge.",
Func: fe.restart,
})
go func() {
defer panicHandler.HandlePanic()
fe.watchEvents()
}()
fe.eventListener.RetryEmit(events.TLSCertIssue)
fe.eventListener.RetryEmit(events.ErrorEvent)
return fe
}
func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent)
addressChangedCh := f.getEventChannel(events.AddressChangedEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue)
for {
select {
case errorDetails := <-errorCh:
f.Println("Bridge failed:", errorDetails)
case <-internetOffCh:
f.notifyInternetOff()
case <-internetOnCh:
f.notifyInternetOn()
case address := <-addressChangedCh:
f.Printf("Address changed for %s. You may need to reconfigure your email client.", address)
case address := <-addressChangedLogoutCh:
f.notifyLogout(address)
case userID := <-logoutCh:
user, err := f.bridge.GetUser(userID)
if err != nil {
return
}
f.notifyLogout(user.Username())
case <-certIssue:
f.notifyCertIssue()
}
}
}
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
return ch
}
// IsAppRestarting returns whether the app is currently set to restart.
func (f *frontendCLI) IsAppRestarting() bool {
return f.appRestart
}
// Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop(credentialsError error) error {
if credentialsError != nil {
f.notifyCredentialsError()
return credentialsError
}
f.preferences.SetBool(preferences.FirstStartKey, false)
f.Print(`
Welcome to ProtonMail Bridge interactive shell
___....___
^^ __..-:'':__:..:__:'':-..__
_.-:__:.-:'': : : :'':-.:__:-._
.':.-: : : : : : : : : :._:'.
_ :.': : : : : : : : : : : :'.: _
[ ]: : : : : : : : : : : : : :[ ]
[ ]: : : : : : : : : : : : : :[ ]
:::::::::[ ]:__:__:__:__:__:__:__:__:__:__:__:__:__:[ ]:::::::::::
!!!!!!!!![ ]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![ ]!!!!!!!!!!!
^^^^^^^^^[ ]^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[ ]^^^^^^^^^^^
[ ] [ ]
[ ] [ ]
jgs [ ] [ ]
~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^
`)
f.Run()
return nil
}

View File

@ -0,0 +1,164 @@
// 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 cli
import (
"fmt"
"strconv"
"strings"
"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/abiosoft/ishell"
)
var (
currentPort = "" //nolint[gochecknoglobals]
)
func (f *frontendCLI) restart(c *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to restart the Bridge") {
f.Println("Restarting Bridge...")
f.appRestart = true
f.Stop()
}
}
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if connection.CheckInternetConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact server please check you internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) {
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
}
func (f *frontendCLI) printManual(c *ishell.Context) {
f.Println("More instructions about the Bridge can be found at\n\n https://protonmail.com/bridge")
}
func (f *frontendCLI) deleteCache(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
if !f.yesNoQuestion("Do you really want to remove all stored preferences") {
return
}
if err := f.bridge.ClearData(); err != nil {
f.printAndLogError("Cache clear failed: ", err.Error())
return
}
f.Println("Cached cleared, restarting bridge")
// Clearing data removes everything (db, preferences, ...)
// so everything has to be stopped and started again.
f.appRestart = true
f.Stop()
}
func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
isSSL := f.preferences.GetBool(preferences.SMTPSSLKey)
newSecurity := "SSL"
if isSSL {
newSecurity = "STARTTLS"
}
msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q and restart the Bridge", newSecurity)
if f.yesNoQuestion(msg) {
f.preferences.SetBool(preferences.SMTPSSLKey, !isSSL)
f.Println("Restarting Bridge...")
f.appRestart = true
f.Stop()
}
}
func (f *frontendCLI) changePort(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
currentPort = f.preferences.Get(preferences.IMAPPortKey)
newIMAPPort := f.readStringInAttempts("Set IMAP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
if newIMAPPort == "" {
newIMAPPort = currentPort
}
imapPortChanged := newIMAPPort != currentPort
currentPort = f.preferences.Get(preferences.SMTPPortKey)
newSMTPPort := f.readStringInAttempts("Set SMTP port (current "+currentPort+")", c.ReadLine, f.isPortFree)
if newSMTPPort == "" {
newSMTPPort = currentPort
}
smtpPortChanged := newSMTPPort != currentPort
if newIMAPPort == newSMTPPort {
f.Println("SMTP and IMAP ports must be different!")
return
}
if imapPortChanged || smtpPortChanged {
f.Println("Saving values IMAP:", newIMAPPort, "SMTP:", newSMTPPort)
f.preferences.Set(preferences.IMAPPortKey, newIMAPPort)
f.preferences.Set(preferences.SMTPPortKey, newSMTPPort)
f.Println("Restarting Bridge...")
f.appRestart = true
f.Stop()
} else {
f.Println("Nothing changed")
}
}
func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
if f.preferences.GetBool(preferences.AllowProxyKey) {
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.preferences.SetBool(preferences.AllowProxyKey, false)
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 {
port = strings.Replace(port, ":", "", -1)
if port == "" || port == currentPort {
return true
}
number, err := strconv.Atoi(port)
if err != nil || number < 0 || number > 65535 {
f.Println("Input", port, "is not a valid port number.")
return false
}
if !ports.IsPortFree(number) {
f.Println("Port", number, "is occupied by another process.")
return false
}
return true
}

View File

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

View File

@ -0,0 +1,123 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
import (
"strings"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/fatih/color"
)
const (
maxInputRepeat = 2
)
var (
bold = color.New(color.Bold).SprintFunc() //nolint[gochecknoglobals]
)
func isNotEmpty(val string) bool {
return val != ""
}
func (f *frontendCLI) yesNoQuestion(question string) bool {
f.Print(question, "? yes/"+bold("no")+": ")
yes := "yes"
answer := strings.ToLower(f.ReadLine())
for i := 0; i < len(answer); i++ {
if i >= len(yes) || answer[i] != yes[i] {
return false // Everything else is false.
}
}
return len(answer) > 0 // Empty is false.
}
func (f *frontendCLI) readStringInAttempts(title string, readFunc func() string, isOK func(string) bool) (value string) {
f.Printf("%s: ", title)
value = readFunc()
title = strings.ToLower(string(title[0])) + title[1:]
for i := 0; !isOK(value); i++ {
if i >= maxInputRepeat {
f.Println("Too many attempts")
return ""
}
f.Printf("Please fill %s: ", title)
value = readFunc()
}
return
}
func (f *frontendCLI) printAndLogError(args ...interface{}) {
log.Error(args...)
f.Println(args...)
}
func (f *frontendCLI) processAPIError(err error) {
log.Warn("API error: ", err)
switch err {
case pmapi.ErrAPINotReachable:
f.notifyInternetOff()
case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade()
default:
f.Println("Server error:", err.Error())
}
}
func (f *frontendCLI) notifyInternetOff() {
f.Println("Internet connection is not available.")
}
func (f *frontendCLI) notifyInternetOn() {
f.Println("Internet connection is available again.")
}
func (f *frontendCLI) notifyLogout(address string) {
f.Printf("Account %s is disconnected. Login to continue using this account with email client.", address)
}
func (f *frontendCLI) notifyNeedUpgrade() {
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
}
func (f *frontendCLI) notifyCredentialsError() {
// Print in 80-column width.
f.Println("ProtonMail 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("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 Bridge 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

@ -0,0 +1,87 @@
// 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 frontend provides all interfaces of the Bridge.
package frontend
import (
"github.com/0xAX/notificator"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
var (
log = config.GetLogEntry("frontend") // nolint[unused]
)
// Frontend is an interface to be implemented by each frontend type (cli, gui, html).
type Frontend interface {
Loop(credentialsError error) error
IsAppRestarting() bool
}
// HandlePanic handles panics which occur for users with GUI.
func HandlePanic() {
notify := notificator.New(notificator.Options{
DefaultIcon: "../frontend/ui/icon/icon.png",
AppName: "ProtonMail Bridge",
})
_ = notify.Push("Fatal Error", "The ProtonMail Bridge has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
}
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
func New(
version,
buildVersion,
frontendType string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
eventListener listener.Listener,
updates types.Updater,
bridge *bridge.Bridge,
noEncConfirmator types.NoEncConfirmator,
) Frontend {
bridgeWrap := types.NewBridgeWrap(bridge)
return new(version, buildVersion, frontendType, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridgeWrap, noEncConfirmator)
}
func new(
version,
buildVersion,
frontendType string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
config *config.Config,
preferences *config.Preferences,
eventListener listener.Listener,
updates types.Updater,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
) Frontend {
switch frontendType {
case "cli":
return cli.New(panicHandler, config, preferences, eventListener, updates, bridge)
default:
return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
}
}

View File

@ -0,0 +1,430 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.8
import ProtonUI 1.0
import BridgeUI 1.0
// NOTE: Keep the Column so the height and width is inherited from content
Column {
id: root
state: status
anchors.left: parent.left
property int row_width: 50 * Style.px
property int row_height: Style.accounts.heightAccount
property var listalias : aliases.split(";")
property int iAccount: index
Accessible.role: go.goos=="windows" ? Accessible.Grouping : Accessible.Row
Accessible.name: qsTr("Account %1, status %2", "Accessible text describing account row with arguments: account name and status (connected/disconnected), resp.").arg(account).arg(statusMark.text)
Accessible.description: Accessible.name
Accessible.ignored: !enabled || !visible
// Main row
Rectangle {
id: mainaccRow
anchors.left: parent.left
width : row_width
height : row_height
state: { return isExpanded ? "expanded" : "collapsed" }
color: Style.main.background
property string actionName : (
isExpanded ?
qsTr("Collapse row for account %2", "Accessible text of button showing additional configuration of account") :
qsTr("Expand row for account %2", "Accessible text of button hiding additional configuration of account")
). arg(account)
// override by other buttons
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked : {
if (root.state=="connected") {
mainaccRow.toggle_accountSettings()
}
}
cursorShape : root.state == "connected" ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true
onEntered: {
if (mainaccRow.state=="collapsed") {
mainaccRow.color = Qt.lighter(Style.main.background,1.1)
}
}
onExited: {
if (mainaccRow.state=="collapsed") {
mainaccRow.color = Style.main.background
}
}
}
// toggle down/up icon
Text {
id: toggleIcon
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
leftMargin : Style.main.leftMargin
}
color: Style.main.text
font {
pointSize : Style.accounts.sizeChevron * Style.pt
family : Style.fontawesome.name
}
text: Style.fa.chevron_down
MouseArea {
anchors.fill: parent
Accessible.role: Accessible.Button
Accessible.name: mainaccRow.actionName
Accessible.description: mainaccRow.actionName
Accessible.onPressAction : mainaccRow.toggle_accountSettings()
Accessible.ignored: root.state!="connected" || !root.enabled
}
}
// account name
TextMetrics {
id: accountMetrics
font : accountName.font
elide: Qt.ElideMiddle
elideWidth: Style.accounts.elideWidth
text: account
}
Text {
id: accountName
anchors {
verticalCenter : parent.verticalCenter
left : toggleIcon.left
leftMargin : Style.main.leftMargin
}
color: Style.main.text
font {
pointSize : (Style.main.fontSize+2*Style.px) * Style.pt
}
text: accountMetrics.elidedText
}
// status
ClickIconText {
id: statusMark
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : Style.accounts.leftMargin2
}
text : qsTr("connected", "status of a listed logged-in account")
iconText : Style.fa.circle_o
textColor : Style.main.textGreen
enabled : false
Accessible.ignored: true
}
// logout
ClickIconText {
id: logoutAccount
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : Style.accounts.leftMargin3
}
text : qsTr("Log out", "action to log out a connected account")
iconText : Style.fa.power_off
textBold : true
textColor : Style.main.textBlue
}
// remove
ClickIconText {
id: deleteAccount
anchors {
verticalCenter : parent.verticalCenter
right : parent.right
rightMargin : Style.main.rightMargin
}
text : qsTr("Remove", "deletes an account from the account settings page")
iconText : Style.fa.trash_o
textColor : Style.main.text
onClicked : {
dialogGlobal.input=root.iAccount
dialogGlobal.state="deleteUser"
dialogGlobal.show()
}
}
// functions
function toggle_accountSettings() {
if (root.state=="connected") {
if (mainaccRow.state=="collapsed" ) {
mainaccRow.state="expanded"
} else {
mainaccRow.state="collapsed"
}
}
}
states: [
State {
name: "collapsed"
PropertyChanges { target : toggleIcon ; text : root.state=="connected" ? Style.fa.chevron_down : " " }
PropertyChanges { target : accountName ; font.bold : false }
PropertyChanges { target : mainaccRow ; color : Style.main.background }
PropertyChanges { target : addressList ; visible : false }
},
State {
name: "expanded"
PropertyChanges { target : toggleIcon ; text : Style.fa.chevron_up }
PropertyChanges { target : accountName ; font.bold : true }
PropertyChanges { target : mainaccRow ; color : Style.accounts.backgroundExpanded }
PropertyChanges { target : addressList ; visible : true }
}
]
}
// List of adresses
Column {
id: addressList
anchors.left : parent.left
width: row_width
visible: false
property alias model : repeaterAddresses.model
Rectangle {
id: addressModeWrapper
anchors {
left : parent.left
right : parent.right
}
visible : mainaccRow.state=="expanded"
height : 2*Style.accounts.heightAddrRow/3
color : Style.accounts.backgroundExpanded
ClickIconText {
id: addressModeSwitch
anchors {
top : addressModeWrapper.top
right : addressModeWrapper.right
rightMargin : Style.main.rightMargin
}
textColor : Style.main.textBlue
iconText : Style.fa.exchange
iconOnRight : false
text : isCombinedAddressMode ?
qsTr("Switch to split addresses mode", "Text of button switching to mode with one configuration per each address.") :
qsTr("Switch to combined addresses mode", "Text of button switching to mode with one configuration for all addresses.")
onClicked: {
dialogGlobal.input=root.iAccount
dialogGlobal.state="addressmode"
dialogGlobal.show()
}
}
ClickIconText {
id: combinedAddressConfig
anchors {
top : addressModeWrapper.top
left : addressModeWrapper.left
leftMargin : Style.accounts.leftMarginAddr+Style.main.leftMargin
}
visible : isCombinedAddressMode
text : qsTr("Mailbox configuration", "Displays IMAP/SMTP settings information for a given account")
iconText : Style.fa.gear
textColor : Style.main.textBlue
onClicked : {
infoWin.showInfo(root.iAccount,0)
}
}
}
Repeater {
id: repeaterAddresses
model: ["one", "two"]
Rectangle {
id: addressRow
visible: !isCombinedAddressMode
anchors {
left : parent.left
right : parent.right
}
height: Style.accounts.heightAddrRow
color: Style.accounts.backgroundExpanded
// icon level down
Text {
id: levelDown
anchors {
left : parent.left
leftMargin : Style.accounts.leftMarginAddr
verticalCenter : wrapAddr.verticalCenter
}
text : Style.fa.level_up
font.family : Style.fontawesome.name
color : Style.main.textDisabled
rotation : 90
}
Rectangle {
id: wrapAddr
anchors {
top : parent.top
left : levelDown.right
right : parent.right
leftMargin : Style.main.leftMargin
rightMargin : Style.main.rightMargin
}
height: Style.accounts.heightAddr
border {
width : Style.main.border
color : Style.main.line
}
color: Style.accounts.backgroundAddrRow
TextMetrics {
id: addressMetrics
font: address.font
elideWidth: 2*wrapAddr.width/3
elide: Qt.ElideMiddle
text: modelData
}
Text {
id: address
anchors {
verticalCenter : parent.verticalCenter
left: parent.left
leftMargin: Style.main.leftMargin
}
font.pointSize : Style.main.fontSize * Style.pt
color: Style.main.text
text: addressMetrics.elidedText
}
ClickIconText {
id: addressConfig
anchors {
verticalCenter : parent.verticalCenter
right: parent.right
rightMargin: Style.main.rightMargin
}
text : qsTr("Address configuration", "Display the IMAP/SMTP configuration for address")
iconText : Style.fa.gear
textColor : Style.main.textBlue
onClicked : infoWin.showInfo(root.iAccount,index)
Accessible.description: qsTr("Address configuration for %1", "Accessible text of button displaying the IMAP/SMTP configuration for address %1").arg(modelData)
Accessible.ignored: !enabled
}
MouseArea {
id: clickSettings
anchors.fill: wrapAddr
onClicked : addressConfig.clicked()
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onPressed: {
wrapAddr.color = Qt.rgba(1,1,1,0.20)
}
onEntered: {
wrapAddr.color = Qt.rgba(1,1,1,0.15)
}
onExited: {
wrapAddr.color = Style.accounts.backgroundAddrRow
}
}
}
}
}
}
Rectangle {
id: line
color: Style.accounts.line
height: Style.accounts.heightLine
width: root.row_width
}
states: [
State {
name: "connected"
PropertyChanges {
target : addressList
model : listalias
}
PropertyChanges {
target : toggleIcon
color : Style.main.text
}
PropertyChanges {
target : accountName
color : Style.main.text
}
PropertyChanges {
target : statusMark
textColor : Style.main.textGreen
text : qsTr("connected", "status of a listed logged-in account")
iconText : Style.fa.circle
}
PropertyChanges {
target : logoutAccount
text : qsTr("Log out", "action to log out a connected account")
onClicked : {
mainaccRow.state="collapsed"
dialogGlobal.input = root.iAccount
dialogGlobal.state = "logout"
dialogGlobal.show()
dialogGlobal.confirmed()
}
}
},
State {
name: "disconnected"
PropertyChanges {
target : addressList
model : 0
}
PropertyChanges {
target : toggleIcon
color : Style.main.textDisabled
}
PropertyChanges {
target : accountName
color : Style.main.textDisabled
}
PropertyChanges {
target : statusMark
textColor : Style.main.textDisabled
text : qsTr("disconnected", "status of a listed logged-out account")
iconText : Style.fa.circle_o
}
PropertyChanges {
target : logoutAccount
text : qsTr("Log in", "action to log in a disconnected account")
onClicked : {
dialogAddUser.username = root.listalias[0]
dialogAddUser.show()
dialogAddUser.inputPassword.focusInput = true
}
}
}
]
}

View File

@ -0,0 +1,72 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Dialog with main menu
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Rectangle {
id: root
color: "#aaff5577"
anchors {
left : tabbar.left
right : tabbar.right
top : tabbar.bottom
bottom : parent.bottom
}
visible: false
MouseArea {
anchors.fill: parent
onClicked: toggle()
}
Rectangle {
color : Style.menu.background
radius : Style.menu.radius
width : Style.menu.width
height : Style.menu.height
anchors {
top : parent.top
right : parent.right
topMargin : Style.menu.topMargin
rightMargin : Style.menu.rightMargin
}
MouseArea {
anchors.fill: parent
}
Text {
anchors.centerIn: parent
text: qsTr("About")
color: Style.menu.text
}
}
function toggle(){
if (root.visible == false) {
root.visible = true
} else {
root.visible = false
}
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Item {
Rectangle {
anchors.centerIn: parent
width: Style.main.width
height: 3*Style.main.height/4
color: "transparent"
//color: "red"
ListView {
anchors.fill: parent
clip : true
model : go.credits.split(";")
delegate: AccessibleText {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData
color: Style.main.text
font.pointSize : Style.main.fontSize * Style.pt
}
footer: ButtonRounded {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Close", "close window")
onClicked: dialogCredits.hide()
}
}
}
}

View File

@ -0,0 +1,124 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import ProtonUI 1.0
Dialog {
id: root
title : ""
isDialogBusy: false
property string firstParagraph : qsTr("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.", "instructions that appear on welcome screen at first start")
property string secondParagraph : qsTr("To add your ProtonMail account to the Bridge and <strong>generate your Bridge password</strong>, please see <a href=\"https://protonmail.com/bridge/install\">the installation guide</a> for detailed setup instructions.", "confirms and dismisses a notification (URL that leads to installation guide should stay intact)")
Column {
id: dialogMessage
property int heightInputs : welcome.height + middleSep.height + instructions.height + buttSep.height + buttonOkay.height + imageSep.height + logo.height
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogMessage.heightInputs)/2 }
Text {
id:welcome
color: Style.main.text
font.bold: true
font.pointSize: 1.5*Style.main.fontSize*Style.pt
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
text: qsTr("Welcome to the", "welcome screen that appears on first start")
}
Rectangle {id: imageSep; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator }
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
Image {
id: logo
anchors.bottom : pmbridge.baseline
height : 2*Style.main.fontSize
fillMode : Image.PreserveAspectFit
mipmap : true
source : "../ProtonUI/images/pm_logo.png"
}
AccessibleText {
id:pmbridge
color: Style.main.text
font.bold: true
font.pointSize: 2.2*Style.main.fontSize*Style.pt
horizontalAlignment: Text.AlignHCenter
text: qsTr("ProtonMail Bridge", "app title")
Accessible.name: this.clearText(pmbridge.text)
Accessible.description: this.clearText(welcome.text+ " " + pmbridge.text + ". " + root.firstParagraph + ". " + root.secondParagraph)
}
}
Rectangle { id:middleSep; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator }
Text {
id:instructions
color: Style.main.text
font.pointSize: Style.main.fontSize*Style.pt
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
width: root.width/1.5
wrapMode: Text.Wrap
textFormat: Text.RichText
text: "<html><style>a { color: "+Style.main.textBlue+"; text-decoration: none;}</style><body>"+
root.firstParagraph +
"<br/><br/>"+
root.secondParagraph +
"</body></html>"
onLinkActivated: {
Qt.openUrlExternally(link)
}
}
Rectangle { id:buttSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
ButtonRounded {
id:buttonOkay
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.hide()
anchors.horizontalCenter: parent.horizontalCenter
}
}
timer.interval : 3000
Connections {
target: timer
onTriggered: {
}
}
onShow : {
pmbridge.Accessible.selected = true
}
}

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/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
import QtQuick.Controls 2.2 as QC
Dialog {
id: root
title : "Set IMAP & SMTP settings"
subtitle : "Changes require reconfiguration of Mail client. (Bridge will automatically restart)"
isDialogBusy: currentIndex==1
Column {
id: dialogMessage
property int heightInputs : imapPort.height + middleSep.height + smtpPort.height + buttonSep.height + buttonRow.height + secSMTPSep.height + securitySMTP.height
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogMessage.heightInputs)/1.6 }
InputField {
id: imapPort
iconText : Style.fa.hashtag
label : qsTr("IMAP port", "entry field to choose port used for the IMAP server")
text : "undef"
}
Rectangle { id:middleSep; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator }
InputField {
id: smtpPort
iconText : Style.fa.hashtag
label : qsTr("SMTP port", "entry field to choose port used for the SMTP server")
text : "undef"
}
Rectangle { id:secSMTPSep; color : Style.transparent; width : Style.main.dummy; height : Style.dialog.heightSeparator }
// SSL button group
Rectangle {
anchors.horizontalCenter : parent.horizontalCenter
width : Style.dialog.widthInput
height : securitySMTPLabel.height + securitySMTP.height
color : "transparent"
AccessibleText {
id: securitySMTPLabel
anchors.left : parent.left
text:qsTr("SMTP connection mode")
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
}
QC.ButtonGroup {
buttons: securitySMTP.children
}
Row {
id: securitySMTP
spacing: Style.dialog.spacing
anchors.top: securitySMTPLabel.bottom
anchors.topMargin: Style.dialog.fontSize
CheckBoxLabel {
id: securitySMTPSSL
text: qsTr("SSL")
}
CheckBoxLabel {
checked: true
id: securitySMTPSTARTTLS
text: qsTr("STARTTLS")
}
}
}
Rectangle { id:buttonSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main: Style.dialog.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()
}
}
}
Column {
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-answ.height)/2 }
Text {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
width : parent.width/2
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
text : "IMAP: " + imapPort.text + "\nSMTP: " + smtpPort.text + "\nSMTP Connection Mode: " + getSelectedSSLMode() + "\n\n" +
qsTr("Settings will be applied after the next start. You will need to reconfigure your email client(s).", "after user changes their ports they will see this notification to reconfigure their setup") +
"\n\n" +
qsTr("Bridge will now restart.", "after user changes their ports this appears to notify the user of restart")
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
}
function areInputsOK() {
var isOK = true
var imapUnchanged = false
var secSMTPUnchanged = (securitySMTPSTARTTLS.checked == go.isSMTPSTARTTLS())
root.warning.text = ""
if (imapPort.text!=go.getIMAPPort()) {
if (go.isPortOpen(imapPort.text)!=0) {
imapPort.rightIcon = Style.fa.exclamation_triangle
root.warning.text = qsTr("Port number is not available.", "if the user changes one of their ports to a port that is occupied by another application")
isOK=false
} else {
imapPort.rightIcon = Style.fa.check_circle
}
} else {
imapPort.rightIcon = ""
imapUnchanged = true
}
if (smtpPort.text!=go.getSMTPPort()) {
if (go.isPortOpen(smtpPort.text)!=0) {
smtpPort.rightIcon = Style.fa.exclamation_triangle
root.warning.text = qsTr("Port number is not available.", "if the user changes one of their ports to a port that is occupied by another application")
isOK=false
} else {
smtpPort.rightIcon = Style.fa.check_circle
}
} else {
smtpPort.rightIcon = ""
if (imapUnchanged && secSMTPUnchanged) {
root.warning.text = qsTr("Please change at least one port number or SMTP security.", "if the user tries to change IMAP/SMTP ports to the same ports as before")
isOK=false
}
}
if (imapPort.text == smtpPort.text) {
smtpPort.rightIcon = Style.fa.exclamation_triangle
root.warning.text = qsTr("Port numbers must be different.", "if the user sets both the IMAP and SMTP ports to the same number")
isOK=false
}
root.warning.visible = !isOK
return isOK
}
function confirmed() {
if (areInputsOK()) {
incrementCurrentIndex()
timer.start()
}
}
function getSelectedSSLMode() {
if (securitySMTPSTARTTLS.checked == true) {
return "STARTTLS"
} else {
return "SSL"
}
}
onShow : {
imapPort.text = go.getIMAPPort()
smtpPort.text = go.getSMTPPort()
if (go.isSMTPSTARTTLS()) {
securitySMTPSTARTTLS.checked = true
} else {
securitySMTPSSL.checked = true
}
areInputsOK()
root.warning.visible = false
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
timer.interval : 3000
Connections {
target: timer
onTriggered: {
go.setPortsAndSecurity(imapPort.text, smtpPort.text, securitySMTPSTARTTLS.checked)
go.isRestarting = true
Qt.quit()
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Dialog {
id: root
title: qsTr("Connection security error", "Title of modal explainning TLS issue")
property string par1Title : qsTr("Description:", "Title of paragraph describing the issue")
property string par1Text : qsTr (
"ProtonMail Bridge 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.",
"A paragraph describing the issue"
)
property string par2Title : qsTr("Recommendation:", "Title of paragraph describing recommended steps")
property string par2Text : qsTr (
"If you are on a corporate or public network, the network administrator may be monitoring or intercepting all traffic.",
"A paragraph describing network issue"
)
property string par2ul1 : qsTr(
"If you trust your network operator, you can continue to use ProtonMail as usual.",
"A list item describing recomendation for trusted network"
)
property string par2ul2 : qsTr(
"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.",
"A list item describing recomendation for untrusted network"
)
property string par3Text : qsTr("Learn more on our knowledge base article","A paragraph describing where to find more information")
property string kbArticleText : qsTr("What is TLS certificate error.", "Link text for knowledge base article")
property string kbArticleLink : "https://protonmail.com/support/knowledge-base/"
Item {
AccessibleText {
anchors.centerIn: parent
color: Style.old.pm_white
linkColor: color
width: parent.width - 50 * Style.px
wrapMode: Text.WordWrap
font.pointSize: Style.main.fontSize*Style.pt
onLinkActivated: Qt.openUrlExternally(link)
text: "<h3>"+par1Title+"</h3>"+
par1Text+"<br>\n"+
"<h3>"+par2Title+"</h3>"+
par2Text+
"<ul>"+
"<li>"+par2ul1+"</li>"+
"<li>"+par2ul2+"</li>"+
"</ul>"+"<br>\n"+
""
//par3Text+
//" <a href='"+kbArticleLink+"'>"+kbArticleText+"</a>\n"
}
}
}

View File

@ -0,0 +1,382 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Dialog {
id: root
title : ""
property string input
property alias question : msg.text
property alias note : noteText.text
property alias answer : answ.text
property alias buttonYes : buttonYes
property alias buttonNo : buttonNo
isDialogBusy: currentIndex==1
signal confirmed()
Column {
id: dialogMessage
property int heightInputs : msg.height+
middleSep.height+
buttonRow.height +
(checkboxSep.visible ? checkboxSep.height : 0 ) +
(noteSep.visible ? noteSep.height : 0 ) +
(checkBoxWrapper.visible ? checkBoxWrapper.height : 0 ) +
(root.note!="" ? noteText.height : 0 )
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogMessage.heightInputs)/2 }
AccessibleText {
id:noteText
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: false
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Rectangle { id: noteSep; visible: note!=""; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
AccessibleText {
id: msg
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
width: 2*parent.width/3
text : ""
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Rectangle { id: checkboxSep; visible: checkBoxWrapper.visible; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
Row {
id: checkBoxWrapper
property bool isChecked : false
visible: root.state=="deleteUser"
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
function toggle() {
checkBoxWrapper.isChecked = !checkBoxWrapper.isChecked
}
Text {
id: checkbox
font {
pointSize : Style.dialog.iconSize * Style.pt
family : Style.fontawesome.name
}
anchors.verticalCenter : parent.verticalCenter
text: checkBoxWrapper.isChecked ? Style.fa.check_square_o : Style.fa.square_o
color: checkBoxWrapper.isChecked ? Style.main.textBlue : Style.main.text
MouseArea {
anchors.fill: parent
onPressed: checkBoxWrapper.toggle()
cursorShape: Qt.PointingHandCursor
}
}
Text {
id: checkBoxNote
anchors.verticalCenter : parent.verticalCenter
text: qsTr("Additionally delete all stored preferences and data", "when removing an account, this extra preference additionally deletes all cached data")
color: Style.main.text
font.pointSize: Style.dialog.fontSize * Style.pt
MouseArea {
anchors.fill: parent
onPressed: checkBoxWrapper.toggle()
cursorShape: Qt.PointingHandCursor
Accessible.role: Accessible.CheckBox
Accessible.checked: checkBoxWrapper.isChecked
Accessible.name: checkBoxNote.text
Accessible.description: checkBoxNote.text
Accessible.ignored: checkBoxNote.text == ""
Accessible.onToggleAction: checkBoxWrapper.toggle()
Accessible.onPressAction: checkBoxWrapper.toggle()
}
}
}
Rectangle { id: middleSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main: Style.dialog.text
fa_icon: Style.fa.times
text: qsTr("No")
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("Yes")
onClicked : {
currentIndex=1
root.confirmed()
}
}
}
}
Column {
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-answ.height)/2 }
AccessibleText {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
color: Style.old.pm_white
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
width: 3*parent.width/4
horizontalAlignment: Text.AlignHCenter
text : qsTr("Waiting...", "in general this displays between screens when processing data takes a long time")
wrapMode: Text.Wrap
}
}
states : [
State {
name: "quit"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Close Bridge", "quits the application")
question : qsTr("Are you sure you want to close the Bridge?", "asked when user tries to quit the application")
note : ""
answer : qsTr("Closing Bridge...", "displayed when user is quitting application")
}
},
State {
name: "logout"
PropertyChanges {
target: root
currentIndex : 1
title : qsTr("Logout", "title of page that displays during account logout")
question : ""
note : ""
answer : qsTr("Logging out...", "displays during account logout")
}
},
State {
name: "deleteUser"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Delete account", "title of page that displays during account deletion")
question : qsTr("Are you sure you want to remove this account?", "displays during account deletion")
note : ""
answer : qsTr("Deleting ...", "displays during account deletion")
}
},
State {
name: "clearChain"
PropertyChanges {
target : root
currentIndex : 0
title : qsTr("Clear keychain", "title of page that displays during keychain clearing")
question : qsTr("Are you sure you want to clear your keychain?", "displays during keychain clearing")
note : qsTr("This will remove all accounts that you have added to the Bridge and disconnect you from your email client(s).", "displays during keychain clearing")
answer : qsTr("Clearing the keychain ...", "displays during keychain clearing")
}
},
State {
name: "clearCache"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Clear cache", "title of page that displays during cache clearing")
question : qsTr("Are you sure you want to clear your local cache?", "displays during cache clearing")
note : qsTr("This will delete all of your stored preferences 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")
}
},
State {
name: "checkUpdates"
PropertyChanges {
target: root
currentIndex : 1
title : ""
question : ""
note : ""
answer : qsTr("Checking for updates ...", "displays if user clicks the Check for Updates button in the Help tab")
}
},
State {
name: "addressmode"
PropertyChanges {
target: root
currentIndex : 0
title : ""
question : qsTr("Do you want to continue?", "asked when the user changes between split and combined address mode")
note : qsTr("Changing between split and combined address mode will require you to delete your account(s) from your email client and begin the setup process from scratch.", "displayed when the user changes between split and combined address mode")
answer : qsTr("Configuring address mode...", "displayed when the user changes between split and combined address mode")
}
},
State {
name: "toggleAutoStart"
PropertyChanges {
target: root
currentIndex : 1
question : ""
note : ""
title : ""
answer : {
var msgTurnOn = qsTr("Turning on automatic start of Bridge...", "when the automatic start feature is selected")
var msgTurnOff = qsTr("Turning off automatic start of Bridge...", "when the automatic start feature is deselected")
return go.isAutoStart==false ? msgTurnOff : msgTurnOn
}
}
},
State {
name: "toggleAllowProxy"
PropertyChanges {
target: root
currentIndex : 0
question : {
var questionTurnOn = qsTr("Do you want to allow alternative routing?")
var questionTurnOff = qsTr("Do you want to disallow alternative routing?")
return go.isProxyAllowed==false ? questionTurnOn : questionTurnOff
}
note : qsTr("In case Proton sites are blocked, this setting allows Bridge to try alternative network routing to reach Proton, which can be useful for bypassing firewalls or network issues. We recommend keeping this setting on for greater reliability.")
title : {
var titleTurnOn = qsTr("Allow alternative routing")
var titleTurnOff = qsTr("Disallow alternative routing")
return go.isProxyAllowed==false ? titleTurnOn : titleTurnOff
}
answer : {
var msgTurnOn = qsTr("Allowing Bridge to use alternative routing to connect to Proton...", "when the allow proxy feature is selected")
var msgTurnOff = qsTr("Disallowing Bridge to use alternative routing to connect to Proton...", "when the allow proxy feature is deselected")
return go.isProxyAllowed==false ? msgTurnOn : msgTurnOff
}
}
},
State {
name: "noKeychain"
PropertyChanges {
target: root
currentIndex : 0
note : qsTr(
"%1 is not able to detected a supported password manager (pass, gnome-keyring). Please install and setup supported password manager and restart the application.",
"Error message when no keychain is detected"
).arg(go.programTitle)
question : qsTr("Do you want to close application now?", "when no password manager found." )
title : "No system password manager detected"
answer : qsTr("Closing Bridge...", "displayed when user is quitting application")
}
},
State {
name: "undef";
PropertyChanges {
target: root
currentIndex : 1
question : ""
note : ""
title : ""
answer : ""
}
}
]
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
onHide: {
checkBoxWrapper.isChecked = false
state = "undef"
}
onShow: {
// hide all other dialogs
winMain.dialogAddUser .visible = false
winMain.dialogChangePort .visible = false
winMain.dialogCredits .visible = false
winMain.dialogVersionInfo .visible = false
// dialogFirstStart should reappear again after closing global
root.visible = true
}
onConfirmed : {
if (state == "quit" || state == "instance exists" ) {
timer.interval = 1000
} else {
timer.interval = 300
}
answ.forceActiveFocus()
timer.start()
}
Connections {
target: timer
onTriggered: {
if ( state == "addressmode" ) { go.switchAddressMode (input) }
if ( state == "clearChain" ) { go.clearKeychain () }
if ( state == "clearCache" ) { go.clearCache () }
if ( state == "deleteUser" ) { go.deleteAccount (input, checkBoxWrapper.isChecked) }
if ( state == "logout" ) { go.logoutAccount (input) }
if ( state == "toggleAutoStart" ) { go.toggleAutoStart () }
if ( state == "toggleAllowProxy" ) { go.toggleAllowProxy () }
if ( state == "quit" ) { Qt.quit () }
if ( state == "instance exists" ) { Qt.quit () }
if ( state == "noKeychain" ) { Qt.quit () }
if ( state == "checkUpdates" ) { go.runCheckVersion (true) }
}
}
Keys.onPressed: {
if (event.key == Qt.Key_Enter) {
root.confirmed()
}
}
}

View File

@ -0,0 +1,134 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List the settings
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
Item {
id: root
// must have wrapper
Rectangle {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
color: Style.main.background
// content
Column {
anchors.horizontalCenter : parent.horizontalCenter
ButtonIconText {
id: logs
anchors.left: parent.left
text: qsTr("Logs", "title of button that takes user to logs directory")
leftIcon.text : Style.fa.align_justify
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: go.openLogs()
}
ButtonIconText {
id: bugreport
anchors.left: parent.left
text: qsTr("Report Bug", "title of button that takes user to bug report form")
leftIcon.text : Style.fa.bug
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: bugreportWin.show()
}
ButtonIconText {
id: manual
anchors.left: parent.left
text: qsTr("Setup Guide", "title of button that opens setup and installation guide")
leftIcon.text : Style.fa.book
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: go.openManual()
}
ButtonIconText {
id: updates
anchors.left: parent.left
text: qsTr("Check for Updates", "title of button to check for any app updates")
leftIcon.text : Style.fa.refresh
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: {
dialogGlobal.state="checkUpdates"
dialogGlobal.show()
dialogGlobal.confirmed()
}
}
// Bottom version notes
Rectangle {
anchors.horizontalCenter : parent.horizontalCenter
height: viewAccount.separatorNoAccount - 3.2*manual.height
width: wrapper.width
color : "transparent"
AccessibleText {
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
color: Style.main.textDisabled
horizontalAlignment: Qt.AlignHCenter
font.pointSize : Style.main.fontSize * Style.pt
text:
"ProtonMail Bridge "+go.getBackendVersion()+"\n"+
"© 2020 Proton Technologies AG"
}
}
Row {
anchors.left : parent.left
Rectangle { height: Style.dialog.spacing; width: (wrapper.width- credits.width - release.width - sepaCreditsRelease.width)/2; color: "transparent"}
ClickIconText {
id:credits
iconText : ""
text : qsTr("Credits", "link to click on to view list of credited libraries")
textColor : Style.main.textDisabled
fontSize : Style.main.fontSize
textUnderline : true
onClicked : winMain.dialogCredits.show()
}
Rectangle {id: sepaCreditsRelease ; height: Style.dialog.spacing; width: Style.main.dummy; color: "transparent"}
ClickIconText {
id:release
iconText : ""
text : qsTr("Release notes", "link to click on to view release notes for this version of the app")
textColor : Style.main.textDisabled
fontSize : Style.main.fontSize
textUnderline : true
onClicked : {
go.getLocalVersionInfo()
winMain.dialogVersionInfo.show()
}
}
}
}
}
}

View File

@ -0,0 +1,144 @@
// 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/>.
// Window for imap and smtp settings
import QtQuick 2.8
import QtQuick.Window 2.2
import BridgeUI 1.0
import ProtonUI 1.0
Window {
id:root
width : Style.info.width
height : Style.info.height
minimumWidth : Style.info.width
minimumHeight : Style.info.height
maximumWidth : Style.info.width
maximumHeight : Style.info.height
color: "transparent"
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
title : address
Accessible.role: Accessible.Window
Accessible.name: qsTr("Configuration information for %1").arg(address)
Accessible.description: Accessible.name
property QtObject accData : QtObject { // avoid null-pointer error
property string account : "undef"
property string aliases : "undef"
property string hostname : "undef"
property string password : "undef"
property int portIMAP : 0
property int portSMTP : 0
}
property string address : "undef"
property int indexAccount : 0
property int indexAddress : 0
WindowTitleBar {
id: titleBar
window: root
}
Rectangle { // background
color: Style.main.background
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
bottom : parent.bottom
}
border {
width: Style.main.border
color: Style.tabbar.background
}
}
// info content
Column {
anchors {
left: parent.left
top: titleBar.bottom
leftMargin: Style.main.leftMargin
topMargin: Style.info.topMargin
}
width : root.width - Style.main.leftMargin - Style.main.rightMargin
TextLabel { text: qsTr("IMAP SETTINGS", "title of the portion of the configuration screen that contains IMAP settings"); state: "heading" }
Rectangle { width: parent.width; height: Style.info.topMargin; color: "#00000000"}
Grid {
columns: 2
rowSpacing: Style.main.fontSize
TextLabel { text: qsTr("Hostname", "in configuration screen, displays the server hostname (127.0.0.1)") + ":"} TextValue { text: root.accData.hostname }
TextLabel { text: qsTr("Port", "in configuration screen, displays the server port (ex. 1025)") + ":"} TextValue { text: root.accData.portIMAP }
TextLabel { text: qsTr("Username", "in configuration screen, displays the username to use with the desktop client") + ":"} TextValue { text: root.address }
TextLabel { text: qsTr("Password", "in configuration screen, displays the Bridge password to use with the desktop client") + ":"} TextValue { text: root.accData.password }
TextLabel { text: qsTr("Security", "in configuration screen, displays the IMAP security settings") + ":"} TextValue { text: "STARTTLS" }
}
Rectangle { width: Style.main.dummy; height: Style.main.fontSize; color: "#00000000"}
Rectangle { width: Style.main.dummy; height: Style.info.topMargin; color: "#00000000"}
TextLabel { text: qsTr("SMTP SETTINGS", "title of the portion of the configuration screen that contains SMTP settings"); state: "heading" }
Rectangle { width: Style.main.dummy; height: Style.info.topMargin; color: "#00000000"}
Grid {
columns: 2
rowSpacing: Style.main.fontSize
TextLabel { text: qsTr("Hostname", "in configuration screen, displays the server hostname (127.0.0.1)") + ":"} TextValue { text: root.accData.hostname }
TextLabel { text: qsTr("Port", "in configuration screen, displays the server port (ex. 1025)") + ":"} TextValue { text: root.accData.portSMTP }
TextLabel { text: qsTr("Username", "in configuration screen, displays the username to use with the desktop client") + ":"} TextValue { text: root.address }
TextLabel { text: qsTr("Password", "in configuration screen, displays the Bridge password to use with the desktop client") + ":"} TextValue { text: root.accData.password }
TextLabel { text: qsTr("Security", "in configuration screen, displays the SMTP security settings") + ":"} TextValue { text: go.isSMTPSTARTTLS() ? "STARTTLS" : "SSL" }
}
Rectangle { width: Style.main.dummy; height: Style.main.fontSize; color: "#00000000"}
Rectangle { width: Style.main.dummy; height: Style.info.topMargin; color: "#00000000"}
}
// apple mail button
ButtonRounded{
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: Style.info.topMargin
}
color_main : Style.main.textBlue
isOpaque: false
text: qsTr("Configure Apple Mail", "button on configuration screen to automatically configure Apple Mail")
height: Style.main.fontSize*2
width: 2*parent.width/3
onClicked: {
go.configureAppleMail(root.indexAccount, root.indexAddress)
}
visible: go.goos == "darwin"
}
function showInfo(iAccount, iAddress) {
root.indexAccount = iAccount
root.indexAddress = iAddress
root.accData = accountsModel.get(iAccount)
root.address = accData.aliases.split(";")[iAddress]
root.show()
root.raise()
root.requestActivate()
}
function hide() {
root.visible = false
}
}

View File

@ -0,0 +1,455 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// This is main window
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import BridgeUI 1.0
import ProtonUI 1.0
// Main Window
Window {
id: root
property alias tabbar : tabbar
property alias viewContent : viewContent
property alias viewAccount : viewAccount
property alias dialogAddUser : dialogAddUser
property alias dialogChangePort : dialogChangePort
property alias dialogCredits : dialogCredits
property alias dialogTlsCert : dialogTlsCert
property alias dialogUpdate : dialogUpdate
property alias dialogFirstStart : dialogFirstStart
property alias dialogGlobal : dialogGlobal
property alias dialogVersionInfo : dialogVersionInfo
property alias dialogConnectionTroubleshoot : dialogConnectionTroubleshoot
property alias bubbleNote : bubbleNote
property alias addAccountTip : addAccountTip
property alias updateState : infoBar.state
property alias tlsBarState : tlsBar.state
property int heightContent : height-titleBar.height
// main window appeareance
width : Style.main.width
height : Style.main.height
flags : Qt.Window | Qt.FramelessWindowHint
color: go.goos=="windows" ? "black" : "transparent"
title: go.programTitle
minimumWidth: Style.main.width
minimumHeight: Style.main.height
maximumWidth: Style.main.width
property bool isOutdateVersion : root.updateState == "forceUpdate"
property bool activeContent :
!dialogAddUser .visible &&
!dialogChangePort .visible &&
!dialogCredits .visible &&
!dialogTlsCert .visible &&
!dialogUpdate .visible &&
!dialogFirstStart .visible &&
!dialogGlobal .visible &&
!dialogVersionInfo .visible &&
!bubbleNote .visible
Accessible.role: Accessible.Grouping
Accessible.description: qsTr("Window %1").arg(title)
Accessible.name: Accessible.description
Component.onCompleted : {
gui.winMain = root
console.log("GraphicsInfo of", titleBar,
"api" , titleBar.GraphicsInfo.api ,
"majorVersion" , titleBar.GraphicsInfo.majorVersion ,
"minorVersion" , titleBar.GraphicsInfo.minorVersion ,
"profile" , titleBar.GraphicsInfo.profile ,
"renderableType" , titleBar.GraphicsInfo.renderableType ,
"shaderCompilationType" , titleBar.GraphicsInfo.shaderCompilationType ,
"shaderSourceType" , titleBar.GraphicsInfo.shaderSourceType ,
"shaderType" , titleBar.GraphicsInfo.shaderType)
tabbar.focusButton()
}
WindowTitleBar {
id: titleBar
window: root
}
Rectangle {
anchors {
top : titleBar.bottom
left : parent.left
right : parent.right
bottom : parent.bottom
}
color: Style.title.background
}
TLSCertPinIssueBar {
id: tlsBar
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
leftMargin: Style.main.border
rightMargin: Style.main.border
}
enabled : root.activeContent
}
InformationBar {
id: infoBar
anchors {
left : parent.left
right : parent.right
top : tlsBar.bottom
leftMargin: Style.main.border
rightMargin: Style.main.border
}
enabled : root.activeContent
}
TabLabels {
id: tabbar
currentIndex : 0
enabled: root.activeContent
anchors {
top : infoBar.bottom
right : parent.right
left : parent.left
leftMargin: Style.main.border
rightMargin: Style.main.border
}
model: [
{ "title" : qsTr("Accounts" , "title of tab that shows account list" ), "iconText": Style.fa.user_circle_o },
{ "title" : qsTr("Settings" , "title of tab that allows user to change settings" ), "iconText": Style.fa.cog },
{ "title" : qsTr("Help" , "title of tab that shows the help menu" ), "iconText": Style.fa.life_ring }
]
}
// Content of tabs
StackLayout {
id: viewContent
enabled: root.activeContent
// dimensions
anchors {
left : parent.left
right : parent.right
top : tabbar.bottom
bottom : parent.bottom
leftMargin: Style.main.border
rightMargin: Style.main.border
bottomMargin: Style.main.border
}
// attributes
currentIndex : { return root.tabbar.currentIndex}
clip : true
// content
AccountView {
id: viewAccount
onAddAccount: dialogAddUser.show()
model: accountsModel
delegate: AccountDelegate {
row_width: viewContent.width
}
}
SettingsView { id: viewSettings; }
HelpView { id: viewHelp; }
}
// Floating things
// Triangle
Rectangle {
id: tabtriangle
visible: false
property int margin : Style.main.leftMargin+ Style.tabbar.widthButton/2
anchors {
top : tabbar.bottom
left : tabbar.left
leftMargin : tabtriangle.margin - tabtriangle.width/2 + tabbar.currentIndex * tabbar.spacing
}
width: 2*Style.tabbar.heightTriangle
height: Style.tabbar.heightTriangle
color: "transparent"
Canvas {
anchors.fill: parent
onPaint: {
var ctx = getContext("2d")
ctx.fillStyle = Style.tabbar.background
ctx.moveTo(0 , 0)
ctx.lineTo(width/2, height)
ctx.lineTo(width , 0)
ctx.closePath()
ctx.fill()
}
}
}
// Bubble prevent action
Rectangle {
anchors {
left: parent.left
right: parent.right
top: titleBar.bottom
bottom: parent.bottom
}
visible: bubbleNote.visible
color: "#aa222222"
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
}
BubbleNote {
id : bubbleNote
visible : false
Component.onCompleted : {
bubbleNote.place(0)
}
}
BubbleNote {
id:addAccountTip
anchors.topMargin: viewAccount.separatorNoAccount - 2*Style.main.fontSize
text : qsTr("Click here to start", "on first launch, this is displayed above the Add Account button to tell the user what to do first")
state: (go.isFirstStart && viewAccount.numAccounts==0 && root.viewContent.currentIndex==0) ? "Visible" : "Invisible"
bubbleColor: Style.main.textBlue
Component.onCompleted : {
addAccountTip.place(-1)
}
enabled: false
states: [
State {
name: "Visible"
// hack: opacity 100% makes buttons dialog windows quit wrong color
PropertyChanges{target: addAccountTip; opacity: 0.999; visible: true}
},
State {
name: "Invisible"
PropertyChanges{target: addAccountTip; opacity: 0.0; visible: false}
}
]
transitions: [
Transition {
from: "Visible"
to: "Invisible"
SequentialAnimation{
NumberAnimation {
target: addAccountTip
property: "opacity"
duration: 0
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: addAccountTip
property: "visible"
duration: 0
}
}
},
Transition {
from: "Invisible"
to: "Visible"
SequentialAnimation{
NumberAnimation {
target: addAccountTip
property: "visible"
duration: 300
}
NumberAnimation {
target: addAccountTip
property: "opacity"
duration: 500
easing.type: Easing.InOutQuad
}
}
}
]
}
// Dialogs
DialogFirstStart {
id: dialogFirstStart
visible: go.isFirstStart && gui.isFirstWindow && !dialogGlobal.visible
}
// Dialogs
DialogPortChange {
id: dialogChangePort
}
DialogConnectionTroubleshoot {
id: dialogConnectionTroubleshoot
}
DialogAddUser {
id: dialogAddUser
onCreateAccount: Qt.openUrlExternally("https://protonmail.com/signup")
}
DialogUpdate {
id: dialogUpdate
property string manualLinks : {
var out = ""
var links = go.downloadLink.split("\n")
var l;
for (l in links) {
out += '<a href="%1">%1</a><br>'.arg(links[l])
}
return out
}
title: root.isOutdateVersion ?
qsTr("%1 is outdated", "title of outdate dialog").arg(go.programTitle):
qsTr("%1 update to %2", "title of update dialog").arg(go.programTitle).arg(go.newversion)
introductionText: {
if (root.isOutdateVersion) {
if (go.goos=="linux") {
return qsTr('You are using an outdated version of our software.<br>
Please download and install the latest version to continue using %1.<br><br>
%2',
"Message for force-update in Linux").arg(go.programTitle).arg(dialogUpdate.manualLinks)
} else {
return qsTr('You are using an outdated version of our software.<br>
Please download and install the latest version to continue using %1.<br><br>
You can continue with the update or download and install the new version manually from<br><br>
<a href="%2">%2</a>',
"Message for force-update in Win/Mac").arg(go.programTitle).arg(go.landingPage)
}
} else {
if (go.goos=="linux") {
return qsTr('A new version of Bridge is available.<br>
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
Use your package manager to update or download and install the new version manually from<br><br>
%3',
"Message for update in Linux").arg("releaseNotes").arg(go.newversion).arg(dialogUpdate.manualLinks)
} else {
return qsTr('A new version of Bridge is available.<br>
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
You can continue with the update or download and install new version manually from<br><br>
<a href="%3">%3</a>',
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
}
}
}
}
Dialog {
id: dialogCredits
title: qsTr("Credits", "link to click on to view list of credited libraries")
Credits { }
}
DialogTLSCertInfo {
id: dialogTlsCert
}
Dialog {
id: dialogVersionInfo
property bool checkVersionOnClose : false
title: qsTr("Information about", "title of release notes page") + " v" + go.newversion
VersionInfo { }
onShow : {
// Hide information bar with old version
if (infoBar.state=="oldVersion") {
infoBar.state="upToDate"
dialogVersionInfo.checkVersionOnClose = true
}
}
onHide : {
// Reload current version based on online status
if (dialogVersionInfo.checkVersionOnClose) go.runCheckVersion(false)
dialogVersionInfo.checkVersionOnClose = false
}
}
DialogYesNo {
id: dialogGlobal
question : ""
answer : ""
z: 100
}
// resize
MouseArea {
property int diff: 0
anchors {
bottom : parent.bottom
left : parent.left
right : parent.right
}
cursorShape: Qt.SizeVerCursor
height: Style.main.fontSize
onPressed: {
var globPos = mapToGlobal(mouse.x, mouse.y)
diff = root.height
diff -= globPos.y
}
onMouseYChanged : {
var globPos = mapToGlobal(mouse.x, mouse.y)
root.height = Math.max(root.minimumHeight, globPos.y + diff)
}
}
function showAndRise(){
go.loadAccounts()
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
// Toggle window
function toggle() {
go.loadAccounts()
if (root.visible) {
if (!root.active) {
root.raise()
root.requestActivate()
} else {
root.hide()
}
} else {
root.show()
root.raise()
}
}
onClosing: {
close.accepted = false
// NOTE: In order to make an initial accounts load
root.hide()
gui.closeMainWindow()
}
}

View File

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

View File

@ -0,0 +1,148 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Popup
import QtQuick 2.8
import QtQuick.Window 2.2
import BridgeUI 1.0
import ProtonUI 1.0
Window {
id:root
width : Style.info.width
height : Style.info.width/1.5
minimumWidth : Style.info.width
minimumHeight : Style.info.width/1.5
maximumWidth : Style.info.width
maximumHeight : Style.info.width/1.5
color : Style.main.background
flags : Qt.Window | Qt.Popup | Qt.FramelessWindowHint
visible : false
title : ""
x: 10
y: 10
property string messageID: ""
// Drag and move
MouseArea {
property point diff: "0,0"
property QtObject window: root
anchors {
fill: parent
}
onPressed: {
diff = Qt.point(window.x, window.y)
var mousePos = mapToGlobal(mouse.x, mouse.y)
diff.x -= mousePos.x
diff.y -= mousePos.y
}
onPositionChanged: {
var currPos = mapToGlobal(mouse.x, mouse.y)
window.x = currPos.x + diff.x
window.y = currPos.y + diff.y
}
}
Column {
topPadding: Style.main.fontSize
spacing: (root.height - (description.height + cancel.height + countDown.height + Style.main.fontSize))/3
width: root.width
Text {
id: description
color : Style.main.text
font.pointSize : Style.main.fontSize*Style.pt/1.2
anchors.horizontalCenter : parent.horizontalCenter
horizontalAlignment : Text.AlignHCenter
width : root.width - 2*Style.main.leftMargin
wrapMode : Text.Wrap
textFormat : Text.RichText
text: qsTr("The message with subject %1 has one or more recipients with no encryption settings. If you do not want to send this email click the cancel button.").arg("<h3>"+root.title+"</h3>")
}
Row {
spacing : Style.dialog.spacing
anchors.horizontalCenter: parent.horizontalCenter
ButtonRounded {
id: cancel
onClicked : root.hide(true)
height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3
fa_icon: Style.fa.send
text: qsTr("Send now", "Confirmation of sending unencrypted email.")
}
ButtonRounded {
id: sendAnyway
onClicked : root.hide(false)
height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3
fa_icon: Style.fa.times
text: qsTr("Cancel", "Cancel the sending of current email")
}
}
Text {
id: countDown
color: Style.main.text
font.pointSize : Style.main.fontSize*Style.pt/1.2
anchors.horizontalCenter : parent.horizontalCenter
horizontalAlignment : Text.AlignHCenter
width : root.width - 2*Style.main.leftMargin
wrapMode : Text.Wrap
textFormat : Text.RichText
text: qsTr("This popup will close after %1 and email will be sent unless you click the cancel button.").arg( "<b>" + timer.secLeft + "s</b>")
}
}
Timer {
id: timer
property var secLeft: 0
interval: 1000 //ms
repeat: true
onTriggered: {
secLeft--
if (secLeft <= 0) {
root.hide(true)
}
}
}
function hide(shouldSend) {
root.visible = false
timer.stop()
go.saveOutgoingNoEncPopupCoord(root.x, root.y)
go.shouldSendAnswer(root.messageID, shouldSend)
}
function show(messageID, subject) {
root.messageID = messageID
root.title = subject
root.visible = true
timer.secLeft = 10
timer.start()
}
}

View File

@ -0,0 +1,180 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List the settings
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
import QtQuick.Controls 2.4
Item {
id: root
// must have wrapper
ScrollView {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: true
background: Rectangle {
color: Style.main.background
}
// content
Column {
anchors.left : parent.left
ButtonIconText {
id: cacheClear
text: qsTr("Clear Cache", "button to clear cache in settings")
leftIcon.text : Style.fa.times
rightIcon {
text : qsTr("Clear", "clickable link next to clear cache button in settings")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogGlobal.state="clearCache"
dialogGlobal.show()
}
}
ButtonIconText {
id: cacheKeychain
text: qsTr("Clear Keychain", "button to clear keychain in settings")
leftIcon.text : Style.fa.chain_broken
rightIcon {
text : qsTr("Clear", "clickable link next to clear keychain button in settings")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogGlobal.state="clearChain"
dialogGlobal.show()
}
}
ButtonIconText {
id: autoStart
text: qsTr("Automatically start Bridge", "label for toggle that activates and disables the automatic start")
leftIcon.text : Style.fa.rocket
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : go.isAutoStart!=false ? Style.fa.toggle_on : Style.fa.toggle_off
color : go.isAutoStart!=false ? Style.main.textBlue : Style.main.textDisabled
}
Accessible.description: (
go.isAutoStart == false ?
qsTr("Enable" , "Click to enable the automatic start of Bridge") :
qsTr("Disable" , "Click to disable the automatic start of Bridge")
) + " " + text
onClicked: {
go.toggleAutoStart()
}
}
ButtonIconText {
id: advancedSettings
property bool isAdvanced : !go.isDefaultPort
text: qsTr("Advanced settings", "button to open the advanced settings list in the settings page")
leftIcon.text : Style.fa.cogs
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : isAdvanced!=0 ? Style.fa.chevron_circle_up : Style.fa.chevron_circle_right
color : isAdvanced!=0 ? Style.main.textDisabled : Style.main.textBlue
}
Accessible.description: (
isAdvanced ?
qsTr("Hide", "Click to hide the advance settings") :
qsTr("Show", "Click to show the advance settings")
) + " " + text
onClicked: {
isAdvanced = !isAdvanced
}
}
ButtonIconText {
id: changePort
visible: advancedSettings.isAdvanced
text: qsTr("Change IMAP & SMTP settings", "button to change IMAP and SMTP ports in settings")
leftIcon.text : Style.fa.plug
rightIcon {
text : qsTr("Change", "clickable link next to change ports button in settings")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogChangePort.show()
}
}
ButtonIconText {
id: reportNoEnc
text: qsTr("Notification of outgoing email without encryption", "Button to set whether to report or send an email without encryption")
visible: advancedSettings.isAdvanced
leftIcon.text : Style.fa.ban
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : go.isReportingOutgoingNoEnc ? Style.fa.toggle_on : Style.fa.toggle_off
color : go.isReportingOutgoingNoEnc ? Style.main.textBlue : Style.main.textDisabled
}
Accessible.description: (
go.isReportingOutgoingNoEnc == 0 ?
qsTr("Enable" , "Click to report an email without encryption") :
qsTr("Disable" , "Click to send without asking an email without encryption")
) + " " + text
onClicked: {
go.toggleIsReportingOutgoingNoEnc()
}
}
ButtonIconText {
id: allowProxy
visible: advancedSettings.isAdvanced
text: qsTr("Allow alternative routing", "label for toggle that allows and disallows using a proxy")
leftIcon.text : Style.fa.rocket
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : go.isProxyAllowed!=false ? Style.fa.toggle_on : Style.fa.toggle_off
color : go.isProxyAllowed!=false ? Style.main.textBlue : Style.main.textDisabled
}
Accessible.description: (
go.isProxyAllowed == false ?
qsTr("Enable" , "Click to allow alternative routing") :
qsTr("Disable" , "Click to disallow alternative routing")
) + " " + text
onClicked: {
dialogGlobal.state="toggleAllowProxy"
dialogGlobal.show()
}
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,15 @@
module BridgeUI
AccountDelegate 1.0 AccountDelegate.qml
Credits 1.0 Credits.qml
DialogFirstStart 1.0 DialogFirstStart.qml
DialogPortChange 1.0 DialogPortChange.qml
DialogYesNo 1.0 DialogYesNo.qml
DialogTLSCertInfo 1.0 DialogTLSCertInfo.qml
HelpView 1.0 HelpView.qml
InfoWindow 1.0 InfoWindow.qml
MainWindow 1.0 MainWindow.qml
ManualWindow 1.0 ManualWindow.qml
OutgoingNoEncPopup 1.0 OutgoingNoEncPopup.qml
SettingsView 1.0 SettingsView.qml
StatusFooter 1.0 StatusFooter.qml
VersionInfo 1.0 VersionInfo.qml

View File

@ -0,0 +1,314 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// This is main qml file
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
// All imports from dynamic must be loaded before
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
Item {
id: gui
property MainWindow winMain
property bool isFirstWindow: true
property int warningFlags: 0
InfoWindow { id: infoWin }
OutgoingNoEncPopup { id: outgoingNoEncPopup }
BugReportWindow {
id: bugreportWin
clientVersion.visible : true
// pre-fill the form
onPrefill : {
userAddress.text=""
if (accountsModel.count>0) {
var addressList = accountsModel.get(0).aliases.split(";")
if (addressList.length>0) {
userAddress.text = addressList[0]
}
}
clientVersion.text=go.getLastMailClient()
}
}
onWarningFlagsChanged : {
if (gui.warningFlags==Style.okInfoBar) {
go.normalSystray()
} else {
if ((gui.warningFlags & Style.errorInfoBar) == Style.errorInfoBar) {
go.errorSystray()
} else {
go.highlightSystray()
}
}
}
// Signals from Go
Connections {
target: go
onShowWindow : {
gui.openMainWindow()
}
onShowHelp : {
gui.openMainWindow(false)
winMain.tabbar.currentIndex = 2
winMain.showAndRise()
}
onShowQuit : {
gui.openMainWindow(false)
winMain.dialogGlobal.state="quit"
winMain.dialogGlobal.show()
winMain.showAndRise()
}
onProcessFinished : {
winMain.dialogGlobal.hide()
winMain.dialogAddUser.hide()
winMain.dialogChangePort.hide()
infoWin.hide()
}
onOpenManual : Qt.openUrlExternally("http://protonmail.com/bridge")
onNotifyBubble : {
gui.showBubble(tabIndex, message, true)
}
onSilentBubble : {
gui.showBubble(tabIndex, message, false)
}
onBubbleClosed : {
gui.warningFlags &= ~Style.warnBubbleMessage
}
onSetConnectionStatus: {
go.isConnectionOK = isAvailable
gui.openMainWindow(false)
if (go.isConnectionOK) {
if( winMain.updateState=="noInternet") {
go.setUpdateState("upToDate")
}
} else {
go.setUpdateState("noInternet")
}
}
onRunCheckVersion : {
gui.openMainWindow(false)
go.setUpdateState("upToDate")
winMain.dialogGlobal.state="checkUpdates"
winMain.dialogGlobal.show()
go.isNewVersionAvailable(showMessage)
}
onSetUpdateState : {
// once app is outdated prevent from state change
if (winMain.updateState != "forceUpdate") {
winMain.updateState = updateState
}
}
onSetAddAccountWarning : winMain.dialogAddUser.setWarning(message, 0)
onNotifyVersionIsTheLatest : {
go.silentBubble(2,qsTr("You have the latest version!", "notification", -1))
}
onNotifyUpdate : {
go.setUpdateState("forceUpdate")
if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
go.runCheckVersion(false)
winMain.dialogUpdate.show()
}
}
onNotifyLogout : {
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Bridge with this account.").arg(accname) )
}
onNotifyAddressChanged : {
go.notifyBubble(0, qsTr("The address list has been changed for account %1. You may need to reconfigure the settings in your email client.").arg(accname) )
}
onNotifyAddressChangedLogout : {
go.notifyBubble(0, qsTr("The address list has been changed for account %1. You have to reconfigure the settings in your email client.").arg(accname) )
}
onNotifyPortIssue : { // busyPortIMAP , busyPortSMTP
if (!busyPortIMAP && !busyPortSMTP) { // at least one must have issues to show warning
return
}
gui.openMainWindow(false)
winMain.tabbar.currentIndex=1
go.isDefaultPort = false
var text
if (busyPortIMAP && busyPortSMTP) { // both have problems
text = qsTr("The default ports used by Bridge for IMAP (%1) and SMTP (%2) are occupied by one or more other applications." , "the first part of notification text (two ports)").arg(go.getIMAPPort()).arg(go.getSMTPPort())
text += " "
text += qsTr("To change the ports for these servers, go to Settings -> Advanced Settings.", "the second part of notification text (two ports)")
} else { // only one is occupied
var server, port
if (busyPortSMTP) {
server = "SMTP"
port = go.getSMTPPort()
} else {
server = "IMAP"
port = go.getIMAPPort()
}
text = qsTr("The default port used by Bridge for %1 (%2) is occupied by another application.", "the first part of notification text (one port)").arg(server).arg(port)
text += " "
text += qsTr("To change the port for this server, go to Settings -> Advanced Settings.", "the second part of notification text (one port)")
}
go.notifyBubble(1, text )
}
onNotifyKeychainRebuild : {
go.notifyBubble(1, qsTr(
"Your MacOS keychain is probably corrupted. Please consult the instructions in our <a href=\"https://protonmail.com/bridge/faq#c15\">FAQ</a>.",
"notification message"
))
}
onNotifyHasNoKeychain : {
gui.winMain.dialogGlobal.state="noKeychain"
gui.winMain.dialogGlobal.show()
}
onShowNoActiveKeyForRecipient : {
go.notifyBubble(0, qsTr(
"Key pinning is enabled for %1 but no active key is pinned. " +
"You must pin the key in order to send a message to this address. " +
"You can find instructions " +
"<a href=\"https://protonmail.com/support/knowledge-base/key-pinning/\">here</a>."
).arg(recipient))
}
onFailedAutostartCode : {
gui.openMainWindow(true)
switch (code) {
case "permission" : // linux+darwin
case "85070005" : // windows
go.notifyBubble(1, go.failedAutostartPerm)
break
case "81004003" : // windows
go.notifyBubble(1, go.failedAutostart+" "+qsTr("Can not create instance.", "for autostart"))
break
case "" :
default :
go.notifyBubble(1, go.failedAutostart)
}
}
onShowOutgoingNoEncPopup : {
outgoingNoEncPopup.show(messageID, subject)
}
onSetOutgoingNoEncPopupCoord : {
outgoingNoEncPopup.x = x
outgoingNoEncPopup.y = y
}
onUpdateFinished : {
winMain.dialogUpdate.finished(hasError)
}
onShowCertIssue : {
winMain.tlsBarState="notOK"
}
}
Timer {
id: checkVersionTimer
repeat : true
triggeredOnStart: false
interval : Style.main.verCheckRepeatTime
onTriggered : go.runCheckVersion(false)
}
function openMainWindow(showAndRise) {
// wait and check until font is loaded
while(true){
if (Style.fontawesome.status == FontLoader.Loading) continue
if (Style.fontawesome.status != FontLoader.Ready) console.log("Error while loading font")
break
}
if (typeof(showAndRise)==='undefined') {
showAndRise = true
}
if (gui.winMain == null) {
gui.winMain = Qt.createQmlObject(
'import BridgeUI 1.0; MainWindow {visible : false}',
gui, "winMain"
)
}
if (showAndRise) {
gui.winMain.showAndRise()
}
}
function closeMainWindow () {
gui.winMain.hide()
gui.winMain.destroy(5000)
gui.winMain = null
gui.isFirstWindow = false
}
function showBubble(tabIndex, message, isWarning) {
gui.openMainWindow(true)
if (isWarning) {
gui.warningFlags |= Style.warnBubbleMessage
}
winMain.bubbleNote.text = message
winMain.bubbleNote.place(tabIndex)
winMain.bubbleNote.show()
}
// On start
Component.onCompleted : {
// set messages for translations
go.wrongCredentials = qsTr("Incorrect username or password." , "notification", -1)
go.wrongMailboxPassword = qsTr("Incorrect mailbox password." , "notification", -1)
go.canNotReachAPI = qsTr("Cannot contact server, please check your internet connection." , "notification", -1)
go.versionCheckFailed = qsTr("Version check was unsuccessful. Please try again later." , "notification", -1)
go.credentialsNotRemoved = qsTr("Credentials could not be removed." , "notification", -1)
go.failedAutostartPerm = qsTr("Unable to configure automatic start due to permissions settings - see <a href=\"https://protonmail.com/bridge/faq#c11\">FAQ</a> for details.", "notification", -1)
go.failedAutostart = qsTr("Unable to configure automatic start." , "notification", -1)
go.genericErrSeeLogs = qsTr("An error happened during procedure. See logs for more details." , "notification", -1)
// start window
gui.openMainWindow(false)
checkVersionTimer.start()
if (go.isShownOnStart) {
gui.winMain.showAndRise()
}
go.runCheckVersion(false)
if (go.isFreshVersion) {
go.getLocalVersionInfo()
gui.winMain.dialogVersionInfo.show()
}
}
}

View File

@ -0,0 +1,34 @@
// 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/>.
// default options to make button accessible
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
Button {
function clearText(value) {
// remove font-awesome chars
return value.replace(/[\uf000-\uf2e0]/g,'')
}
Accessible.onPressAction: clicked()
Accessible.ignored: !enabled || !visible
Accessible.name: clearText(text)
Accessible.description: clearText(text)
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// default options to make text accessible and selectable
import QtQuick 2.8
import ProtonUI 1.0
TextEdit {
function clearText(value) {
// substitue the copyright symbol by the text and remove the font-awesome chars and HTML tags
return value.replace(/\uf1f9/g,'Copyright').replace(/[\uf000-\uf2e0]/g,'').replace(/<[^>]+>/g,'')
}
readOnly: true
selectByKeyboard: true
selectByMouse: true
Accessible.role: Accessible.StaticText
Accessible.name: clearText(text)
Accessible.description: clearText(text)
Accessible.focusable: true
Accessible.ignored: !enabled || !visible || text == ""
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// default options to make text accessible
import QtQuick 2.8
import ProtonUI 1.0
Text {
function clearText(value) {
// substitue the copyright symbol by the text and remove the font-awesome chars and HTML tags
return value.replace(/\uf1f9/g,'Copyright').replace(/[\uf000-\uf2e0]/g,'').replace(/<[^>]+>/g,'')
}
Accessible.role: Accessible.StaticText
Accessible.name: clearText(text)
Accessible.description: clearText(text)
Accessible.focusable: true
Accessible.ignored: !enabled || !visible || text == ""
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}
}

View File

@ -0,0 +1,140 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
Item {
id: root
signal addAccount()
property alias numAccounts : listAccounts.count
property alias model : listAccounts.model
property alias delegate : listAccounts.delegate
property int separatorNoAccount : viewContent.height-Style.accounts.heightFooter
property bool hasFooter : true
// must have wrapper
Rectangle {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
color: Style.main.background
// content
ListView {
id: listAccounts
anchors {
top : parent.top
left : parent.left
right : parent.right
bottom : hasFooter ? addAccFooter.top : parent.bottom
}
orientation: ListView.Vertical
clip: true
cacheBuffer: 2500
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
anchors {
right: parent.right
rightMargin: Style.main.rightMargin/4
}
width: Style.main.rightMargin/3
Accessible.ignored: true
}
header: Rectangle {
width : wrapper.width
height : root.numAccounts!=0 ? Style.accounts.heightHeader : root.separatorNoAccount
color : "transparent"
AccessibleText { // Placeholder on empty
anchors {
centerIn: parent
}
visible: root.numAccounts==0
text : qsTr("No accounts added", "displayed when there are no accounts added")
font.pointSize : Style.main.fontSize * Style.pt
color : Style.main.textDisabled
}
Text { // Account
anchors {
left : parent.left
leftMargin : Style.main.leftMargin
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0
font.bold : true
font.pointSize : Style.main.fontSize * Style.pt
text : qsTr("ACCOUNT", "title of column that displays account name")
color : Style.main.textDisabled
}
Text { // Status
anchors {
left : parent.left
leftMargin : Style.accounts.leftMargin2
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0
font.bold : true
font.pointSize : Style.main.fontSize * Style.pt
text : qsTr("STATUS", "title of column that displays connected or disconnected status")
color : Style.main.textDisabled
}
Text { // Actions
anchors {
left : parent.left
leftMargin : Style.accounts.leftMargin3
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0
font.bold : true
font.pointSize : Style.main.fontSize * Style.pt
text : qsTr("ACTIONS", "title of column that displays log out and log in actions for each account")
color : Style.main.textDisabled
}
// line
Rectangle {
anchors {
left : parent.left
right : parent.right
bottom : parent.bottom
}
visible: root.numAccounts!=0
color: Style.accounts.line
height: Style.accounts.heightLine
}
}
}
AddAccountBar {
id: addAccFooter
visible: hasFooter
anchors {
left : parent.left
bottom : parent.bottom
}
}
}
Shortcut {
sequence: StandardKey.SelectAll
onActivated: root.addAccount()
}
}

View File

@ -0,0 +1,69 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Bar with add account button and help
import QtQuick 2.8
import ProtonUI 1.0
Rectangle {
width : parent.width
height : Style.accounts.heightFooter
color: "transparent"
Rectangle {
anchors {
top : parent.top
left : parent.left
right : parent.right
}
height: Style.accounts.heightLine
color: Style.accounts.line
}
ClickIconText {
id: buttonAddAccount
anchors {
left : parent.left
leftMargin : Style.main.leftMargin
verticalCenter : parent.verticalCenter
}
textColor : Style.main.textBlue
iconText : Style.fa.plus_circle
text : qsTr("Add Account", "begins the flow to log in to an account that is not yet listed")
textBold : true
onClicked : root.addAccount()
Accessible.description: {
if (gui.winMain!=null) {
return text + (gui.winMain.addAccountTip.visible? ", "+gui.winMain.addAccountTip.text : "")
}
return buttonAddAccount.text
}
}
ClickIconText {
id: buttonHelp
anchors {
right : parent.right
rightMargin : Style.main.rightMargin
verticalCenter : parent.verticalCenter
}
textColor : Style.main.textDisabled
iconText : Style.fa.question_circle
text : qsTr("Help", "directs the user to the online user guide")
textBold : true
onClicked : go.openManual()
}
}

View File

@ -0,0 +1,170 @@
// 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/>.
// Notify user
import QtQuick 2.8
import ProtonUI 1.0
Rectangle {
id: root
property int posx // x-coordinate of triangle
property bool isTriangleBelow
property string text
property alias bubbleColor: bubble.color
anchors {
top : tabbar.bottom
left : tabbar.left
leftMargin : {
// position of bubble calculated from posx
return Math.max(
Style.main.leftMargin, // keep minimal left margin
Math.min(
root.posx - root.width/2, // fit triangle in the middle if possible
tabbar.width - root.width - Style.main.rightMargin // keep minimal right margin
)
)
}
topMargin: 0
}
height : triangle.height + bubble.height
width : bubble.width
color : "transparent"
visible : false
Rectangle {
id : triangle
anchors {
top : root.isTriangleBelow ? undefined : root.top
bottom : root.isTriangleBelow ? root.bottom : undefined
bottomMargin : 1*Style.px
left : root.left
leftMargin : root.posx - triangle.width/2 - root.anchors.leftMargin
}
width: 2*Style.tabbar.heightTriangle+2
height: Style.tabbar.heightTriangle
color: "transparent"
Canvas {
anchors.fill: parent
rotation: root.isTriangleBelow ? 180 : 0
onPaint: {
var ctx = getContext("2d")
ctx.fillStyle = bubble.color
ctx.moveTo(0 , height)
ctx.lineTo(width/2, 0)
ctx.lineTo(width , height)
ctx.closePath()
ctx.fill()
}
}
}
Rectangle {
id: bubble
anchors {
top: root.top
left: root.left
topMargin: (root.isTriangleBelow ? 0 : triangle.height)
}
width : mainText.contentWidth + Style.main.leftMargin + Style.main.rightMargin
height : 2*Style.main.fontSize
radius : Style.bubble.radius
color : Style.bubble.background
AccessibleText {
id: mainText
anchors {
horizontalCenter : parent.horizontalCenter
top: parent.top
topMargin : Style.main.fontSize
}
text: "<html><style>a { color: "+Style.main.textBlue+";}</style>"+root.text+"<html>"
width : Style.bubble.width - ( Style.main.leftMargin + Style.main.rightMargin )
font.pointSize: Style.main.fontSize * Style.pt
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
wrapMode: Text.WordWrap
color: Style.bubble.text
onLinkActivated: {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: mainText
acceptedButtons: Qt.NoButton
}
Accessible.name: qsTr("Message")
Accessible.description: root.text
}
ButtonRounded {
id: okButton
visible: !root.isTriangleBelow
anchors {
bottom : parent.bottom
horizontalCenter : parent.horizontalCenter
bottomMargin : Style.main.fontSize
}
text: qsTr("Okay", "confirms and dismisses a notification")
height: Style.main.fontSize*2
color_main: Style.main.text
color_minor: Style.main.textBlue
isOpaque: true
onClicked: hide()
}
}
function place(index) {
if (index < 0) {
// add accounts
root.isTriangleBelow = true
bubble.height = 3.25*Style.main.fontSize
root.posx = 2*Style.main.leftMargin
bubble.width = mainText.contentWidth - Style.main.leftMargin
} else {
root.isTriangleBelow = false
bubble.height = (
bubble.anchors.topMargin + // from top
mainText.contentHeight + // the text content
Style.main.fontSize + // gap between button
okButton.height + okButton.anchors.bottomMargin // from bottom and button
)
if (index < 3) {
// possition accordig to top tab
var margin = Style.main.leftMargin + Style.tabbar.widthButton/2
root.posx = margin + index*tabbar.spacing
} else {
// quit button
root.posx = tabbar.width - 2*Style.main.rightMargin
}
}
}
function show() {
root.visible=true
gui.winMain.activeContent = false
}
function hide() {
root.visible=false
go.bubbleClosed()
gui.winMain.activeContent = true
gui.winMain.tabbar.focusButton()
}
}

View File

@ -0,0 +1,337 @@
// 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/>.
// Window for sending a bug report
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import ProtonUI 1.0
import QtGraphicalEffects 1.0
Window {
id:root
property alias userAddress : userAddress
property alias clientVersion : clientVersion
width : Style.bugreport.width
height : Style.bugreport.height
minimumWidth : Style.bugreport.width
maximumWidth : Style.bugreport.width
minimumHeight : Style.bugreport.height
maximumHeight : Style.bugreport.height
property color inputBorderColor : Style.main.text
color : "transparent"
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
title : "ProtonMail Bridge - Bug report"
visible : false
WindowTitleBar {
id: titleBar
window: root
}
Rectangle {
id:background
color: Style.main.background
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
bottom : parent.bottom
}
border {
width: Style.main.border
color: Style.tabbar.background
}
}
Rectangle {
id:content
anchors {
fill : parent
leftMargin : Style.main.leftMargin
rightMargin : Style.main.rightMargin
bottomMargin : Style.main.rightMargin
topMargin : Style.main.rightMargin + titleBar.height
}
color: "transparent"
// Description in flickable
Flickable {
id: descripWrapper
anchors {
left: parent.left
right: parent.right
top: parent.top
}
height: content.height - (
(clientVersion.visible ? clientVersion.height + Style.dialog.fontSize : 0) +
userAddress.height + Style.dialog.fontSize +
securityNote.contentHeight + Style.dialog.fontSize +
cancelButton.height + Style.dialog.fontSize
)
clip: true
contentWidth : width
contentHeight : height
TextArea.flickable: TextArea {
id: description
focus: true
wrapMode: TextEdit.Wrap
placeholderText: qsTr ("Please briefly describe the bug(s) you have encountered...", "bug report instructions")
background : Rectangle {
color : Style.dialog.background
radius: Style.dialog.radiusButton
border {
color : root.inputBorderColor
width : Style.dialog.borderInput
}
layer.enabled: true
layer.effect: FastBlur {
anchors.fill: parent
radius: 8 * Style.px
}
}
color: Style.main.text
font.pointSize: Style.dialog.fontSize * Style.pt
selectionColor: Style.main.textBlue
selectByKeyboard: true
selectByMouse: true
KeyNavigation.tab: clientVersion
KeyNavigation.priority: KeyNavigation.BeforeItem
}
ScrollBar.vertical : ScrollBar{}
}
// Client
TextLabel {
anchors {
left: parent.left
top: descripWrapper.bottom
topMargin: Style.dialog.fontSize
}
visible: clientVersion.visible
width: parent.width/2.618
text: qsTr ("Email client:", "in the bug report form, which third-party email client is being used")
font.pointSize: Style.dialog.fontSize * Style.pt
}
TextField {
id: clientVersion
anchors {
right: parent.right
top: descripWrapper.bottom
topMargin: Style.dialog.fontSize
}
placeholderText: qsTr("e.g. Thunderbird", "in the bug report form, placeholder text for email client")
width: parent.width/1.618
color : Style.dialog.text
selectionColor : Style.main.textBlue
selectByMouse : true
font.pointSize : Style.dialog.fontSize * Style.pt
padding : Style.dialog.radiusButton
background: Rectangle {
color : Style.dialog.background
radius: Style.dialog.radiusButton
border {
color : root.inputBorderColor
width : Style.dialog.borderInput
}
layer.enabled: true
layer.effect: FastBlur {
anchors.fill: parent
radius: 8 * Style.px
}
}
onAccepted: userAddress.focus = true
}
// Address
TextLabel {
anchors {
left: parent.left
top: clientVersion.visible ? clientVersion.bottom : descripWrapper.bottom
topMargin: Style.dialog.fontSize
}
color: Style.dialog.text
width: parent.width/2.618
text: qsTr ("Contact email:", "in the bug report form, an email to contact the user at")
font.pointSize: Style.dialog.fontSize * Style.pt
}
TextField {
id: userAddress
anchors {
right: parent.right
top: clientVersion.visible ? clientVersion.bottom : descripWrapper.bottom
topMargin: Style.dialog.fontSize
}
placeholderText: "benjerry@protonmail.com"
width: parent.width/1.618
color : Style.dialog.text
selectionColor : Style.main.textBlue
selectByMouse : true
font.pointSize : Style.dialog.fontSize * Style.pt
padding : Style.dialog.radiusButton
background: Rectangle {
color : Style.dialog.background
radius: Style.dialog.radiusButton
border {
color : root.inputBorderColor
width : Style.dialog.borderInput
}
layer.enabled: true
layer.effect: FastBlur {
anchors.fill: parent
radius: 8 * Style.px
}
}
onAccepted: root.submit()
}
// Note
AccessibleText {
id: securityNote
anchors {
left: parent.left
right: parent.right
top: userAddress.bottom
topMargin: Style.dialog.fontSize
}
wrapMode: Text.Wrap
color: Style.dialog.text
font.pointSize : Style.dialog.fontSize * Style.pt
text:
"<span style='font-family: " + Style.fontawesome.name + "'>" + Style.fa.exclamation_triangle + "</span> " +
qsTr("Bug reports are not end-to-end encrypted!", "The first part of warning in bug report form") + " " +
qsTr("Please do not send any sensitive information.", "The second part of warning in bug report form") + " " +
qsTr("Contact us at security@protonmail.com for critical security issues.", "The third part of warning in bug report form")
}
// buttons
ButtonRounded {
id: cancelButton
anchors {
left: parent.left
bottom: parent.bottom
}
fa_icon: Style.fa.times
text: qsTr ("Cancel", "dismisses current action")
onClicked : root.hide()
}
ButtonRounded {
anchors {
right: parent.right
bottom: parent.bottom
}
isOpaque: true
color_main: "white"
color_minor: Style.main.textBlue
fa_icon: Style.fa.send
text: qsTr ("Send", "button sends bug report")
onClicked : root.submit()
}
}
Rectangle {
id: notification
property bool isOK: true
visible: false
color: background.color
anchors.fill: background
Text {
anchors.centerIn: parent
color: Style.dialog.text
width: background.width*0.6180
text: notification.isOK ?
qsTr ( "Bug report successfully sent." , "notification message about bug sending" ) :
qsTr ( "Unable to submit bug report." , "notification message about bug sending" )
horizontalAlignment: Text.AlignHCenter
font.pointSize: Style.dialog.titleSize * Style.pt
}
Timer {
id: notificationTimer
interval: 3000
repeat: false
onTriggered : {
notification.visible=false
if (notification.isOK) root.hide()
}
}
}
function submit(){
if(root.areInputsOK()){
root.notify(go.sendBug(description.text, clientVersion.text, userAddress.text ))
}
}
function isEmpty(input){
if (input.text=="") {
input.focus=true
input.placeholderText = qsTr("Field required", "a field that must be filled in to submit form")
return true
}
return false
}
function areInputsOK() {
var isOK = true
if (isEmpty(userAddress)) { isOK=false }
if (clientVersion.visible && isEmpty(clientVersion)) { isOK=false }
if (isEmpty(description)) { isOK=false }
return isOK
}
function clear() {
description.text = ""
clientVersion.text = ""
notification.visible = false
}
signal prefill()
function notify(isOK){
notification.isOK = isOK
notification.visible = true
notificationTimer.start()
}
function show() {
prefill()
root.visible=true
}
function hide() {
clear()
root.visible=false
}
}

View File

@ -0,0 +1,100 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Button with full window width containing two icons (left and right) and text
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
AccessibleButton {
id: root
property alias leftIcon : leftIcon
property alias rightIcon : rightIcon
property alias main : mainText
// dimensions
width : viewContent.width
height : Style.main.heightRow
topPadding: 0
bottomPadding: 0
leftPadding: Style.main.leftMargin
rightPadding: Style.main.rightMargin
background : Rectangle{
color: Qt.lighter(Style.main.background, root.hovered || root.activeFocus ? ( root.pressed ? 1.2: 1.1) :1.0)
// line
Rectangle {
anchors.bottom : parent.bottom
width : parent.width
height : Style.main.heightLine
color : Style.main.line
}
// pointing cursor
MouseArea {
anchors.fill : parent
cursorShape : Qt.PointingHandCursor
acceptedButtons: Qt.NoButton
}
}
contentItem : Rectangle {
color: "transparent"
// Icon left
Text {
id: leftIcon
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
}
font {
family : Style.fontawesome.name
pointSize : Style.settings.iconSize * Style.pt
}
color : Style.main.textBlue
text : Style.fa.hashtag
}
// Icon/Text right
Text {
id: rightIcon
anchors {
verticalCenter : parent.verticalCenter
right : parent.right
}
font {
family : Style.fontawesome.name
pointSize : Style.settings.iconSize * Style.pt
}
color : Style.main.textBlue
text : Style.fa.hashtag
}
// Label
Text {
id: mainText
anchors {
verticalCenter : parent.verticalCenter
left : leftIcon.right
leftMargin : leftIcon.text!="" ? Style.main.leftMargin : 0
}
font.pointSize : Style.settings.fontSize * Style.pt
color : Style.main.text
text : root.text
}
}
}

View File

@ -0,0 +1,92 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Classic button with icon and text
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtGraphicalEffects 1.0
import ProtonUI 1.0
AccessibleButton {
id: root
property string fa_icon : ""
property color color_main : Style.dialog.text
property color color_minor : "transparent"
property bool isOpaque : false
text : "undef"
state : root.hovered || root.activeFocus ? "hover" : "normal"
width : Style.dialog.widthButton
height : Style.dialog.heightButton
scale : root.pressed ? 0.96 : 1.00
background: Rectangle {
border {
color : root.color_main
width : root.isOpaque ? 0 : Style.dialog.borderButton
}
radius : Style.dialog.radiusButton
color : root.isOpaque ? root.color_minor : "transparent"
MouseArea {
anchors.fill : parent
cursorShape : Qt.PointingHandCursor
acceptedButtons: Qt.NoButton
}
}
contentItem: Rectangle {
color: "transparent"
Row {
id: mainText
anchors.centerIn: parent
spacing: 0
Text {
font {
pointSize : Style.dialog.fontSize * Style.pt
family : Style.fontawesome.name
}
color : color_main
text : root.fa_icon=="" ? "" : root.fa_icon + " "
}
Text {
font {
pointSize : Style.dialog.fontSize * Style.pt
}
color : color_main
text : root.text
}
}
Glow {
id: mainTextEffect
anchors.fill : mainText
source: mainText
color: color_main
opacity: 0.33
}
}
states :[
State {name: "normal"; PropertyChanges{ target: mainTextEffect; radius: 0 ; visible: false } },
State {name: "hover" ; PropertyChanges{ target: mainTextEffect; radius: 3*Style.px ; visible: true } }
]
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
CheckBox {
id: root
spacing: Style.dialog.spacing
padding: 0
property color textColor : Style.main.text
property color checkedColor : Style.main.textBlue
property color uncheckedColor : Style.main.textInactive
property string checkedSymbol : Style.fa.check_square_o
property string uncheckedSymbol : Style.fa.square_o
background: Rectangle {
color: Style.transparent
}
indicator: Text {
text : root.checked ? root.checkedSymbol : root.uncheckedSymbol
color : root.checked ? root.checkedColor : root.uncheckedColor
font {
pointSize : Style.dialog.iconSize * Style.pt
family : Style.fontawesome.name
}
}
contentItem: Text {
id: label
text : root.text
color : root.textColor
font {
pointSize: Style.dialog.fontSize * Style.pt
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: Style.dialog.iconSize + root.spacing
}
}

View File

@ -0,0 +1,98 @@
// 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/>.
// No border button with icon
import QtQuick 2.8
import QtQuick.Controls 2.1
import ProtonUI 1.0
AccessibleButton {
id: root
property string iconText : Style.fa.hashtag
property color textColor : Style.main.text
property int fontSize : Style.main.fontSize
property int iconSize : Style.main.iconSize
property int margin : iconText!="" ? Style.main.leftMarginButton : 0.0
property bool iconOnRight : false
property bool textBold : false
property bool textUnderline : false
TextMetrics {
id: metrics
text: root.text
font: showText.font
}
TextMetrics {
id: metricsIcon
text : root.iconText
font : showIcon.font
}
scale : root.pressed ? 0.96 : root.activeFocus ? 1.05 : 1.0
height : Math.max(metrics.height, metricsIcon.height)
width : metricsIcon.width*1.5 + margin + metrics.width + 4.0
padding : 0.0
background : Rectangle {
color: Style.transparent
MouseArea {
anchors.fill : parent
cursorShape : Qt.PointingHandCursor
acceptedButtons: Qt.NoButton
}
}
contentItem : Rectangle {
color: Style.transparent
Text {
id: showIcon
anchors {
left : iconOnRight ? showText.right : parent.left
leftMargin : iconOnRight ? margin : 0
verticalCenter : parent.verticalCenter
}
font {
pointSize : iconSize * Style.pt
family : Style.fontawesome.name
}
color : textColor
text : root.iconText
}
Text {
id: showText
anchors {
verticalCenter : parent.verticalCenter
left : iconOnRight ? parent.left : showIcon.right
leftMargin : iconOnRight ? 0 : margin
}
color : textColor
font {
pointSize : root.fontSize * Style.pt
bold: root.textBold
underline: root.textUnderline
}
text : root.text
}
}
}

View File

@ -0,0 +1,147 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Dialog with adding new user
import QtQuick 2.8
import QtQuick.Layouts 1.3
import ProtonUI 1.0
StackLayout {
id: root
property string title : "title"
property string subtitle : ""
property alias timer : timer
property alias warning : warningText
property bool isDialogBusy : false
property real titleHeight : 2*titleText.anchors.topMargin + titleText.height + (warningText.visible ? warningText.anchors.topMargin + warningText.height : 0)
property Item background : Rectangle {
parent: root
width: root.width
height: root.height
color : Style.dialog.background
visible: root.visible
z: -1
AccessibleText {
id: titleText
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: Style.dialog.titleSize
}
font.pointSize : Style.dialog.titleSize * Style.pt
color : Style.dialog.text
text : root.title
}
AccessibleText {
id: subtitleText
anchors {
top: titleText.bottom
horizontalCenter: parent.horizontalCenter
}
font.pointSize : Style.dialog.fontSize * Style.pt
color : Style.dialog.text
text : root.subtitle
visible : root.subtitle != ""
}
AccessibleText {
id:warningText
anchors {
top: subtitleText.bottom
horizontalCenter: parent.horizontalCenter
}
font {
bold: true
pointSize: Style.dialog.fontSize * Style.pt
}
text : ""
color: Style.main.textBlue
visible: false
}
// prevent any action below
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
ClickIconText {
anchors {
top: parent.top
right: parent.right
topMargin: Style.dialog.titleSize
rightMargin: Style.dialog.titleSize
}
visible : !isDialogBusy
iconText : Style.fa.times
text : ""
onClicked : root.hide()
Accessible.description : qsTr("Close dialog %1", "Click to exit modal.").arg(root.title)
}
}
Accessible.role: Accessible.Grouping
Accessible.name: title
Accessible.description: title
Accessible.focusable: true
visible : false
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
bottom : parent.bottom
}
currentIndex : 0
signal show()
signal hide()
function incrementCurrentIndex() {
root.currentIndex++
}
function decrementCurrentIndex() {
root.currentIndex--
}
onShow: {
root.visible = true
root.forceActiveFocus()
}
onHide: {
root.timer.stop()
root.currentIndex=0
root.visible = false
root.timer.stop()
gui.winMain.tabbar.focusButton()
}
// QTimer is recommeded solution for creating trheads : http://doc.qt.io/qt-5/qtquick-threading-example.html
Timer {
id: timer
interval: 300 // wait for transistion
repeat: false
}
}

View File

@ -0,0 +1,464 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Dialog with adding new user
import QtQuick 2.8
import ProtonUI 1.0
Dialog {
id: root
title : ""
signal createAccount()
property alias inputPassword : inputPassword
property alias input2FAuth : input2FAuth
property alias inputPasswMailbox : inputPasswMailbox
//
property alias username : inputUsername.text
property alias usernameElided : usernameMetrics.elidedText
isDialogBusy : currentIndex==waitingAuthIndex || currentIndex==addingAccIndex
property bool isFirstAccount: false
property color buttonOpaqueMain : "white"
property int origin: 0
property int nameAndPasswordIndex : 0
property int waitingAuthIndex : 2
property int twoFAIndex : 1
property int mailboxIndex : 3
property int addingAccIndex : 4
property int newAccountIndex : 5
signal cancel()
signal okay()
TextMetrics {
id: usernameMetrics
font: dialogWaitingAuthText.font
elideWidth : Style.dialog.widthInput
elide : Qt.ElideMiddle
text : root.username
}
Column { // 0
id: dialogNameAndPassword
property int heightInputs : inputUsername.height + buttonRow.height + middleSep.height + inputPassword.height + middleSepPassw.height
Rectangle {
id: topSep
color : "transparent"
width : Style.main.dummy
height : root.height/2 - (dialogNameAndPassword.heightInputs)/2
}
InputField {
id: inputUsername
iconText : Style.fa.user_circle
label : qsTr("Username", "enter username to add account")
onAccepted : inputPassword.focusInput = true
}
Rectangle { id: middleSepPassw; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
InputField {
id: inputPassword
label : qsTr("Password", "password entry field")
iconText : Style.fa.lock
isPassword : true
onAccepted : root.okay()
}
Rectangle { id: middleSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.fontSize
ButtonRounded {
id:buttonCancel
fa_icon : Style.fa.times
text : qsTr("Cancel", "dismisses current action")
color_main : Style.dialog.text
onClicked : root.cancel()
}
ButtonRounded {
id: buttonNext
fa_icon : Style.fa.check
text : qsTr("Next", "navigate to next page in add account flow")
color_main : buttonOpaqueMain
color_minor : Style.dialog.textBlue
isOpaque : true
onClicked : root.okay()
}
}
Rectangle {
color : "transparent"
width : Style.main.dummy
height : root.height - (topSep.height + dialogNameAndPassword.heightInputs + Style.main.bottomMargin + signUpForAccount.height)
}
ClickIconText {
id: signUpForAccount
anchors.horizontalCenter: parent.horizontalCenter
fontSize : Style.dialog.fontSize
iconSize : Style.dialog.fontSize
iconText : "+"
text : qsTr ("Sign Up for an Account", "takes user to web page where they can create a ProtonMail account")
textBold : true
textUnderline : true
textColor : Style.dialog.text
onClicked : root.createAccount()
}
}
Column { // 1
id: dialog2FA
property int heightInputs : buttonRowPassw.height + middleSep2FA.height + input2FAuth.height
Rectangle {
color : "transparent"
width : Style.main.dummy
height : (root.height - dialog2FA.heightInputs)/2
}
InputField {
id: input2FAuth
label : qsTr("Two Factor Code", "two factor code entry field")
iconText : Style.fa.lock
onAccepted : root.okay()
}
Rectangle { id: middleSep2FA; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRowPassw
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.fontSize
ButtonRounded {
id: buttonBack
fa_icon: Style.fa.times
text: qsTr("Back", "navigate back in add account flow")
color_main: Style.dialog.text
onClicked : root.cancel()
}
ButtonRounded {
id: buttonNextTwo
fa_icon: Style.fa.check
text: qsTr("Next", "navigate to next page in add account flow")
color_main: buttonOpaqueMain
color_minor: Style.dialog.textBlue
isOpaque: true
onClicked : root.okay()
}
}
}
Column { // 2
id: dialogWaitingAuth
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogWaitingAuthText.height) /2 }
Text {
id: dialogWaitingAuthText
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font.pointSize: Style.dialog.fontSize * Style.pt
text : qsTr("Logging in") +"\n" + root.usernameElided
horizontalAlignment: Text.AlignHCenter
}
}
Column { // 3
id: dialogMailboxPassword
property int heightInputs : buttonRowMailbox.height + inputPasswMailbox.height + middleSepMailbox.height
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height - dialogMailboxPassword.heightInputs)/2}
InputField {
id: inputPasswMailbox
label : qsTr("Mailbox password for %1", "mailbox password entry field").arg(root.usernameElided)
iconText : Style.fa.lock
isPassword : true
onAccepted : root.okay()
}
Rectangle { id: middleSepMailbox; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRowMailbox
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.fontSize
ButtonRounded {
id: buttonBackBack
fa_icon: Style.fa.times
text: qsTr("Back", "navigate back in add account flow")
color_main: Style.dialog.text
onClicked : root.cancel()
}
ButtonRounded {
id: buttonLogin
fa_icon: Style.fa.check
text: qsTr("Next", "navigate to next page in add account flow")
color_main: buttonOpaqueMain
color_minor: Style.dialog.textBlue
isOpaque: true
onClicked : root.okay()
}
}
}
Column { // 4
id: dialogWaitingAccount
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height - dialogWaitingAccountText.height )/2 }
Text {
id: dialogWaitingAccountText
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
bold : true
pointSize: Style.dialog.fontSize * Style.pt
}
text : qsTr("Adding account, please wait ...", "displayed after user has logged in, before new account is displayed")
wrapMode: Text.Wrap
}
}
Column { // 5
id: dialogFirstUserAdded
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height - dialogWaitingAccountText.height - okButton.height*2 )/2 }
Text {
id: textFirstUser
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
bold : false
pointSize: Style.dialog.fontSize * Style.pt
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
text: "<html><style>a { font-weight: bold; text-decoration: none; color: white;}</style>"+
qsTr("Now you need to configure your client(s) to use the Bridge. Instructions for configuring your client can be found at", "") +
"<br/><a href=\"https://protonmail.com/bridge/clients\">https://protonmail.com/bridge/clients</a>.<html>"
wrapMode: Text.Wrap
onLinkActivated: {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink=="" ? Qt.PointingHandCursor : Qt.WaitCursor
acceptedButtons: Qt.NoButton
}
}
Rectangle { color : "transparent"; width : Style.main.dummy; height : okButton.height}
ButtonRounded{
id: okButton
anchors.horizontalCenter: parent.horizontalCenter
color_main: buttonOpaqueMain
color_minor: Style.main.textBlue
isOpaque: true
text: qsTr("Okay", "confirms and dismisses a notification")
onClicked: root.hide()
}
}
function clear_user() {
inputUsername.text = ""
inputUsername.rightIcon = ""
}
function clear_passwd() {
inputPassword.text = ""
inputPassword.rightIcon = ""
inputPassword.hidePasswordText()
}
function clear_2fa() {
input2FAuth.text = ""
input2FAuth.rightIcon = ""
}
function clear_passwd_mailbox() {
inputPasswMailbox.text = ""
inputPasswMailbox.rightIcon = ""
inputPasswMailbox.hidePasswordText()
}
onCancel : {
root.warning.visible=false
if (currentIndex==0) {
root.hide()
} else {
clear_passwd()
clear_passwd_mailbox()
currentIndex=0
}
}
function check_inputs() {
var isOK = true
switch (currentIndex) {
case nameAndPasswordIndex :
isOK &= inputUsername.checkNonEmpty()
isOK &= inputPassword.checkNonEmpty()
break
case twoFAIndex :
isOK &= input2FAuth.checkNonEmpty()
break
case mailboxIndex :
isOK &= inputPasswMailbox.checkNonEmpty()
break
}
if (isOK) {
warning.visible = false
warning.text= ""
} else {
setWarning(qsTr("Field required", "a field that must be filled in to submit form"),0)
}
return isOK
}
function setWarning(msg, changeIndex) {
// show message
root.warning.text = msg
root.warning.visible = true
}
onOkay : {
var isOK = check_inputs()
if (isOK) {
root.origin = root.currentIndex
switch (root.currentIndex) {
case nameAndPasswordIndex:
case twoFAIndex:
root.currentIndex = waitingAuthIndex
break;
case mailboxIndex:
root.currentIndex = addingAccIndex
}
timer.start()
}
}
onShow: {
root.title = qsTr ("Log in to your ProtonMail account", "displayed on screen when user enters username to begin adding account")
root.warning.visible = false
inputUsername.forceFocus()
root.isFirstAccount = go.isFirstStart && accountsModel.count==0
}
function startAgain() {
clear_passwd()
clear_2fa()
clear_passwd_mailbox()
root.currentIndex = nameAndPasswordIndex
root.inputPassword.focusInput = true
}
function finishLogin(){
root.currentIndex = addingAccIndex
var auth = go.addAccount(inputPasswMailbox.text)
if (auth<0) {
startAgain()
return
}
}
Connections {
target: timer
onTriggered : {
timer.repeat = false
switch (root.origin) {
case nameAndPasswordIndex:
var auth = go.login(inputUsername.text, inputPassword.text)
if (auth < 0) {
startAgain()
break
}
if (auth == 1) {
root.currentIndex = twoFAIndex
root.input2FAuth.focusInput = true
break
}
if (auth == 2) {
root.currentIndex = mailboxIndex
root.inputPasswMailbox.focusInput = true
break
}
root.inputPasswMailbox.text = inputPassword.text
root.finishLogin()
break;
case twoFAIndex:
var auth = go.auth2FA(input2FAuth.text)
if (auth < 0) {
startAgain()
break
}
if (auth == 1) {
root.currentIndex = mailboxIndex
root.inputPasswMailbox.focusInput = true
break
}
root.inputPasswMailbox.text = inputPassword.text
root.finishLogin()
break;
case mailboxIndex:
root.finishLogin()
break;
}
}
}
onHide: {
// because hide slot is conneceted to processFinished it will update
// the list evertyime `go` obejcet is finished
clear_passwd()
clear_passwd_mailbox()
clear_2fa()
clear_user()
go.loadAccounts()
if (root.isFirstAccount && accountsModel.count==1) {
root.isFirstAccount=false
root.currentIndex=5
root.show()
root.title=qsTr("Success, Account Added!", "shown after successful account addition")
}
}
Keys.onPressed: {
if (event.key == Qt.Key_Enter) {
root.okay()
}
}
}

View File

@ -0,0 +1,148 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import ProtonUI 1.0
Dialog {
id: root
title : qsTr(
"Common connection problems and solutions",
"Title of the network troubleshooting modal"
)
isDialogBusy: false // can close
property var parContent : [
[
qsTr("Allow alternative routing" , "Paragraph title"),
qsTr(
"In case Proton sites are blocked, this setting allows Bridge "+
"to try alternative network routing to reach Proton, which can "+
"be useful for bypassing firewalls or network issues. We recommend "+
"keeping this setting on for greater reliability. "+
'<a href="https://protonmail.com/blog/anti-censorship-alternative-routing/">Learn more</a>'+
" and "+
'<a href="showProxy">enable here</a>'+
".",
"Paragraph content"
),
],
[
qsTr("No internet connection" , "Paragraph title"),
qsTr(
"Please make sure that your internet connection is working.",
"Paragraph content"
),
],
[
qsTr("Internet Service Provider (ISP) problem" , "Paragraph title"),
qsTr(
"Try connecting to Proton from a different network (or use "+
'<a href="https://protonvpn.com/">ProtonVPN</a>'+
" or "+
'<a href="https://torproject.org/">Tor</a>'+
").",
"Paragraph content"
),
],
[
qsTr("Government block" , "Paragraph title"),
qsTr(
"Your country may be blocking access to Proton. Try using "+
'<a href="https://protonvpn.com/">ProtonVPN</a>'+
" (or any other VPN) or "+
'<a href="https://torproject.org/">Tor</a>'+
".",
"Paragraph content"
),
],
[
qsTr("Antivirus interference" , "Paragraph title"),
qsTr(
"Temporarily disable or remove your antivirus software.",
"Paragraph content"
),
],
[
qsTr("Proxy/Firewall interference" , "Paragraph title"),
qsTr(
"Disable any proxies or firewalls, or contact your network administrator.",
"Paragraph content"
),
],
[
qsTr("Still cant find a solution" , "Paragraph title"),
qsTr(
"Contact us directly through our "+
'<a href="https://protonmail.com/support-form">support form</a>'+
", email (support@protonmail.com), or "+
'<a href="https://twitter.com/ProtonMail">Twitter</a>'+
".",
"Paragraph content"
),
],
[
qsTr("Proton is down" , "Paragraph title"),
qsTr(
"Check "+
'<a href="https://protonstatus.com/">Proton Status</a>'+
" for our system status.",
"Paragraph content"
),
],
]
Item {
AccessibleText {
anchors.centerIn: parent
color: Style.old.pm_white
linkColor: color
width: parent.width - 50 * Style.px
wrapMode: Text.WordWrap
font.pointSize: Style.main.fontSize*Style.pt
onLinkActivated: {
if (link=="showProxy") {
dialogGlobal.state= "toggleAllowProxy"
dialogGlobal.show()
} else {
Qt.openUrlExternally(link)
}
}
text: {
var content=""
for (var i=0; i<root.parContent.length; i++) {
var par = root.parContent[i]
content += "<p>"
content += "<b>"+par[0]+":</b> "
content += par[1]
content += "</p>\n"
}
return content
}
}
}
}

View File

@ -0,0 +1,250 @@
// 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/>.
// default options to make button accessible
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
Dialog {
id: root
title: "Bridge update "+go.newversion
property alias introductionText : introduction.text
property bool hasError : false
signal cancel()
signal okay()
isDialogBusy: currentIndex==1
Rectangle { // 0: Release notes and confirm
width: parent.width
height: parent.height
color: Style.transparent
Column {
anchors.centerIn: parent
spacing: 5*Style.dialog.spacing
AccessibleText {
id:introduction
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
linkColor: Style.dialog.textBlue
font {
pointSize: 0.8 * Style.dialog.fontSize * Style.pt
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
// customize message per application
text: ' <a href="%1">Release notes</a><br> New version %2<br> <br><br> <a href="%3">%3</a>'
onLinkActivated : {
console.log("clicked link:", link)
if (link == "releaseNotes"){
root.hide()
winMain.dialogVersionInfo.show()
} else {
root.hide()
Qt.openUrlExternally(link)
}
}
MouseArea {
anchors.fill: parent
cursorShape: introduction.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
ButtonRounded {
fa_icon: Style.fa.times
text: (go.goos=="linux" ? qsTr("Okay") : qsTr("Cancel"))
color_main: Style.dialog.text
onClicked: root.cancel()
}
ButtonRounded {
fa_icon: Style.fa.check
text: qsTr("Update")
visible: go.goos!="linux"
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
onClicked: root.okay()
}
}
}
}
Rectangle { // 0: Check / download / unpack / prepare
id: updateStatus
width: parent.width
height: parent.height
color: Style.transparent
Column {
anchors.centerIn: parent
spacing: Style.dialog.spacing
AccessibleText {
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: false
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
text: {
switch (go.progressDescription) {
case 1: return qsTr("Checking the current version.")
case 2: return qsTr("Downloading the update files.")
case 3: return qsTr("Verifying the update files.")
case 4: return qsTr("Unpacking the update files.")
case 5: return qsTr("Starting the update.")
case 6: return qsTr("Quitting the application.")
default: return ""
}
}
}
ProgressBar {
id: progressbar
implicitWidth : 2*updateStatus.width/3
implicitHeight : Style.exporting.rowHeight
visible: go.progress!=0 // hack hide animation when clearing out progress bar
value: go.progress
property int current: go.total * go.progress
property bool isFinished: finishedPartBar.width == progressbar.width
background: Rectangle {
radius : Style.exporting.boxRadius
color : Style.exporting.progressBackground
}
contentItem: Item {
Rectangle {
id: finishedPartBar
width : parent.width * progressbar.visualPosition
height : parent.height
radius : Style.exporting.boxRadius
gradient : Gradient {
GradientStop { position: 0.00; color: Qt.lighter(Style.main.textBlue,1.1) }
GradientStop { position: 0.66; color: Style.main.textBlue }
GradientStop { position: 1.00; color: Qt.darker(Style.main.textBlue,1.1) }
}
Behavior on width {
NumberAnimation { duration:300; easing.type: Easing.InOutQuad }
}
}
Text {
anchors.centerIn: parent
text: ""
color: Style.main.background
font {
pointSize: Style.dialog.fontSize * Style.pt
}
}
}
}
}
}
Rectangle { // 1: Something went wrong / All ok, closing bridge
width: parent.width
height: parent.height
color: Style.transparent
Column {
anchors.centerIn: parent
spacing: 5*Style.dialog.spacing
AccessibleText {
color: Style.dialog.text
linkColor: Style.dialog.textBlue
font {
pointSize: Style.dialog.fontSize * Style.pt
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
text: !root.hasError ? qsTr('Application will quit now to finish the update.', "message after successful update") :
qsTr('<b>The update procedure was not successful!</b><br>Please follow the download link and update manually. <br><br><a href="%1">%1</a>').arg(go.downloadLink)
onLinkActivated : {
console.log("clicked link:", link)
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}
}
ButtonRounded{
anchors.horizontalCenter: parent.horizontalCenter
visible: root.hasError
text: qsTr("Close")
onClicked: root.cancel()
}
}
}
function clear() {
root.hasError = false
go.progress = 0.0
go.progressDescription = 0
}
function finished(hasError) {
root.hasError = hasError
root.incrementCurrentIndex()
}
onShow: {
root.clear()
}
onHide: {
root.clear()
}
onOkay: {
switch (root.currentIndex) {
case 0:
go.startUpdate()
}
root.incrementCurrentIndex()
}
onCancel: {
root.hide()
}
}

View File

@ -0,0 +1,78 @@
// 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/>.
// one line input text field with label
import QtQuick 2.8
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.0
import ProtonUI 1.0
Row {
id: root
spacing: Style.dialog.spacing
property string title : "title"
property alias path: inputPath.text
property alias inputPath: inputPath
property alias dialogVisible: pathDialog.visible
InputBox {
id: inputPath
anchors {
bottom: parent.bottom
}
spacing: Style.dialog.spacing
field {
height: browseButton.height
width: root.width - root.spacing - browseButton.width
}
label: title
Component.onCompleted: sanitizePath(pathDialog.shortcuts.home)
}
ButtonRounded {
id: browseButton
anchors {
bottom: parent.bottom
}
height: Style.dialog.heightInput
color_main: Style.main.textBlue
fa_icon: Style.fa.folder_open
text: qsTr("Browse", "click to look through directory for a file or folder")
onClicked: pathDialog.visible = true
}
FileDialog {
id: pathDialog
title: root.title + ":"
folder: shortcuts.home
onAccepted: sanitizePath(pathDialog.fileUrl.toString())
selectFolder: true
}
function sanitizePath(path) {
var pattern = "file://"
if (go.goos=="windows") pattern+="/"
inputPath.text = path.replace(pattern, "")
}
function checkNonEmpty() {
return inputPath.text != ""
}
}

View File

@ -0,0 +1,101 @@
// 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/>.
// on hover information
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
Text { // info icon
id:root
property alias info : tip.text
font {
family: Style.fontawesome.name
pointSize : Style.dialog.iconSize * Style.pt
}
text: Style.fa.info_circle
color: Style.main.textDisabled
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered : tip.visible=true
onExited : tip.visible=false
}
ToolTip {
id: tip
width: Style.bubble.width
x: - 0.2*tip.width
y: - tip.height
topPadding : Style.main.fontSize/2
bottomPadding : Style.main.fontSize/2
leftPadding : Style.bubble.widthPane + Style.dialog.spacing
rightPadding: Style.dialog.spacing
delay: 800
background : Rectangle {
id: bck
color: Style.bubble.paneBackground
radius : Style.bubble.radius
Text {
id: icon
color: Style.bubble.background
text: Style.fa.info_circle
font {
family : Style.fontawesome.name
pointSize : Style.dialog.iconSize * Style.pt
}
anchors {
verticalCenter : bck.verticalCenter
left : bck.left
leftMargin : (Style.bubble.widthPane - icon.width) / 2
}
}
Rectangle { // right edge
anchors {
fill : bck
leftMargin : Style.bubble.widthPane
}
radius: parent.radius
color: Style.bubble.background
}
Rectangle { // center background
anchors {
fill : parent
leftMargin : Style.bubble.widthPane
rightMargin : Style.bubble.widthPane
}
color: Style.bubble.background
}
}
contentItem : Text {
text: tip.text
color: Style.bubble.text
wrapMode: Text.Wrap
font.pointSize: Style.main.fontSize * Style.pt
}
}
}

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/>.
// Important information under title bar
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import ProtonUI 1.0
Rectangle {
id: root
property var iTry: 0
property var secLeft: 0
property var second: 1000 // convert millisecond to second
property var checkInterval: [ 5, 10, 30, 60, 120, 300, 600 ] // seconds
property bool isVisible: true
property var fontSize : 1.2 * Style.main.fontSize
color : "black"
state: "upToDate"
Timer {
id: retryInternet
interval: second
triggeredOnStart: false
repeat: true
onTriggered : {
secLeft--
if (secLeft <= 0) {
retryInternet.stop()
go.checkInternet()
if (iTry < checkInterval.length-1) {
iTry++
}
secLeft=checkInterval[iTry]
retryInternet.start()
}
}
}
Row {
anchors.centerIn: root
visible: root.isVisible
spacing: Style.main.leftMarginButton
AccessibleText {
id: message
font.pointSize: root.fontSize * Style.pt
}
ClickIconText {
anchors.verticalCenter : message.verticalCenter
text : "("+go.newversion+" " + qsTr("release notes", "display the release notes from the new version")+")"
visible : root.state=="oldVersion" && ( go.changelog!="" || go.bugfixes!="")
iconText : ""
onClicked : {
dialogVersionInfo.show()
}
fontSize : root.fontSize
}
ClickIconText {
anchors.verticalCenter : message.verticalCenter
text : root.state=="oldVersion" || root.state == "forceUpdate" ?
qsTr("Update", "click to update to a new version when one is available") :
qsTr("Retry now", "click to try to connect to the internet when the app is disconnected from the internet")
visible : root.state!="internetCheck"
iconText : ""
onClicked : {
if (root.state=="oldVersion" || root.state=="forceUpdate" ) {
winMain.dialogUpdate.show()
} else {
go.checkInternet()
}
}
fontSize : root.fontSize
textUnderline: true
}
Text {
anchors.baseline : message.baseline
color: Style.main.text
font {
pointSize : root.fontSize * Style.pt
bold : true
}
visible: root.state=="oldVersion" || root.state=="noInternet"
text : "|"
}
ClickIconText {
anchors.verticalCenter : message.verticalCenter
iconText : ""
text : root.state == "noInternet" ?
qsTr("Troubleshoot", "Show modal screen with additional tips for troubleshooting connection issues") :
qsTr("Remind me later", "Do not install new version and dismiss a notification")
visible : root.state=="oldVersion" || root.state=="noInternet"
onClicked : {
if (root.state == "oldVersion") {
root.state = "upToDate"
}
if (root.state == "noInternet") {
dialogConnectionTroubleshoot.show()
}
}
fontSize : root.fontSize
textUnderline: true
}
}
onStateChanged : {
switch (root.state) {
case "forceUpdate" :
gui.warningFlags |= Style.errorInfoBar
break;
case "upToDate" :
gui.warningFlags &= ~Style.warnInfoBar
iTry = 0
secLeft=checkInterval[iTry]
break;
case "noInternet" :
gui.warningFlags |= Style.warnInfoBar
retryInternet.start()
secLeft=checkInterval[iTry]
break;
default :
gui.warningFlags |= Style.warnInfoBar
}
if (root.state!="noInternet") {
retryInternet.stop()
}
}
function timeToRetry() {
if (secLeft==1){
return qsTr("a second", "time to wait till internet connection is retried")
} else if (secLeft<60){
return secLeft + " " + qsTr("seconds", "time to wait till internet connection is retried")
} else {
var leading = ""+secLeft%60
if (leading.length < 2) {
leading = "0" + leading
}
return Math.floor(secLeft/60) + ":" + leading
}
}
states: [
State {
name: "internetCheck"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
isVisible: true
color: Style.main.textOrange
}
PropertyChanges {
target: message
color: Style.main.background
text: qsTr("Checking connection. Please wait...", "displayed after user retries internet connection")
}
},
State {
name: "noInternet"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
isVisible: true
color: Style.main.textRed
}
PropertyChanges {
target: message
color: Style.main.line
text: qsTr("Cannot contact server. Retrying in ", "displayed when the app is disconnected from the internet or server has problems")+timeToRetry()+"."
}
},
State {
name: "oldVersion"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
isVisible: true
color: Style.main.textBlue
}
PropertyChanges {
target: message
color: Style.main.background
text: qsTr("An update is available.", "displayed in a notification when an app update is available")
}
},
State {
name: "forceUpdate"
PropertyChanges {
target: root
height: 2* Style.main.fontSize
isVisible: true
color: Style.main.textRed
}
PropertyChanges {
target: message
color: Style.main.line
text: qsTr("%1 is outdated.", "displayed in a notification when app is outdated").arg(go.programTitle)
}
},
State {
name: "upToDate"
PropertyChanges {
target: root
height: 0
isVisible: false
color: Style.main.textBlue
}
PropertyChanges {
target: message
color: Style.main.background
text: ""
}
}
]
}

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