mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
184 Commits
cuthix-iss
...
v1.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 7baa4dc117 | |||
| f073301481 | |||
| bf0945eaef | |||
| 11e01ca163 | |||
| a650a04a88 | |||
| bdc11c8358 | |||
| ed7a0dc9b3 | |||
| fc4e77604f | |||
| abaeace4b3 | |||
| 457b524ba8 | |||
| d89d627349 | |||
| 51ff880fd9 | |||
| b25baa2524 | |||
| 10e384f4df | |||
| 35ae2011b6 | |||
| 5348ae7d18 | |||
| 2512d3647a | |||
| 1e8cb35fcb | |||
| 0b0991d682 | |||
| 813e99f399 | |||
| 7301e5571c | |||
| b6707749e5 | |||
| 7ec4309ae1 | |||
| ec224a962f | |||
| 012be60311 | |||
| 02804d067c | |||
| 9241a9bdbf | |||
| f3e6af5571 | |||
| 7a13b89274 | |||
| 5cb78b0a03 | |||
| c19bb0fa97 | |||
| de16f6f2d1 | |||
| 7963b3c152 | |||
| f82ab3189b | |||
| 49cc49b1e2 | |||
| 9808c44714 | |||
| c329711f9c | |||
| 928fa93765 | |||
| 45e99caa23 | |||
| 80b2bfc2a5 | |||
| 6070a3b7cc | |||
| 9e633400b0 | |||
| 84d344cb0a | |||
| e43033b42b | |||
| e5d63edb62 | |||
| 579e962980 | |||
| 2919d1a3c0 | |||
| 1ba319bb69 | |||
| 8cdebb6d05 | |||
| cc14b523cb | |||
| ad877431de | |||
| 40d8c458d2 | |||
| 3b0b1a457b | |||
| 7ac4c9aecf | |||
| 390182d247 | |||
| cb8a15a9fd | |||
| 4d2baa6b85 | |||
| 7724ca3996 | |||
| 4393d67bf2 | |||
| d222b39793 | |||
| 6ae78217db | |||
| b91c286332 | |||
| 4e2ab9b389 | |||
| c6c6cfc7d7 | |||
| a78b1ca00f | |||
| d718720b29 | |||
| f64cb4b56d | |||
| b2b43ac909 | |||
| f2b8d02cd2 | |||
| 50ed40f205 | |||
| 3c92ff18ff | |||
| 41f6cd3bcd | |||
| 66f23bef99 | |||
| bbf1364e30 | |||
| 6147c214c3 | |||
| f87ca36ffd | |||
| 37f4e46bdc | |||
| a7b9572e6b | |||
| 4090c490b1 | |||
| d33d7237bd | |||
| 9ed778f2b3 | |||
| 70fca64a36 | |||
| 30425d5fcd | |||
| 2639f7333e | |||
| 4a8d07d54e | |||
| 833fce8702 | |||
| c1a57a2e12 | |||
| cda3000a7a | |||
| 4b2977041a | |||
| 2d200f6f8c | |||
| c61e8bdc71 | |||
| 7e8dc22837 | |||
| e43bd231ed | |||
| 1998d92432 | |||
| 313e803fdd | |||
| cabcb3ae2b | |||
| e57a3c2a3a | |||
| 7a87a7ea2f | |||
| c939893131 | |||
| ea0f3115a3 | |||
| 3d3b91b242 | |||
| cd38c86b4b | |||
| 2379598078 | |||
| 984b28e8f9 | |||
| 1d49a484a8 | |||
| 99aabf07b3 | |||
| 6e537db5ff | |||
| 668fc7f039 | |||
| 284a097d4f | |||
| e5944518ca | |||
| df3a9ea19e | |||
| 2db1b113e0 | |||
| 68d2591c73 | |||
| e9735c6110 | |||
| 0fd5ca3a24 | |||
| 8c2f88fe70 | |||
| 23f492705b | |||
| 44233e5bd3 | |||
| 33770ce129 | |||
| faec347054 | |||
| 9b68625522 | |||
| 8288a39ff4 | |||
| b15d22c8cc | |||
| d42deb2ad5 | |||
| bb5227c1f4 | |||
| 0589f329e9 | |||
| 522cadb8b1 | |||
| 32ca7b3903 | |||
| 7d30459417 | |||
| 8f15041d8f | |||
| 51846efed5 | |||
| a1b01d5922 | |||
| 76b480298a | |||
| a51841158c | |||
| 68d1442a8f | |||
| 1457005f86 | |||
| febdf98349 | |||
| d4482994ec | |||
| 244a18ac8c | |||
| e027aa5fae | |||
| 99635cd56d | |||
| e95aece6d3 | |||
| 38f0425670 | |||
| 4809d97cb1 | |||
| bfc4069df4 | |||
| 3f32fd95e0 | |||
| 40e96b9d1e | |||
| 80f4e1e346 | |||
| debd374d75 | |||
| ed8595fa5b | |||
| fec5f2d3c3 | |||
| bafd4e714e | |||
| d787d8b223 | |||
| abca7284dd | |||
| db02eb694d | |||
| 5bf4d9c6f5 | |||
| b01be382fc | |||
| 042c340881 | |||
| f269be4291 | |||
| 6e38a65bd8 | |||
| 941e09079c | |||
| ce29d4d74e | |||
| f239e8f3bf | |||
| 0a55fac29a | |||
| fb263e84a9 | |||
| 366a9d6d6c | |||
| 8f8fbc745d | |||
| b75a6f7cf8 | |||
| 9072f84646 | |||
| c6f32192b9 | |||
| 49a64a656c | |||
| 1c83cc9754 | |||
| 341a6501e6 | |||
| e1ecc11f38 | |||
| d1e63254f2 | |||
| 0998c67f20 | |||
| 91ec7edc06 | |||
| aea816029f | |||
| e166748270 | |||
| 0c7a328165 | |||
| e962434c8f | |||
| 46f3721d43 | |||
| 0cb1ff9b16 | |||
| a246a35cb7 |
38
.github/ISSUE_TEMPLATE/general-issue-template.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/general-issue-template.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: General issue template
|
||||
about: Template for detailed report of issues
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Issue tracker is ONLY used for reporting bugs with technical details. "It doesn't work" or new features should be discussed with our customer support. Please use bug report function in Bridge or contact bridge@protonmail.ch.
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
## Expected Behavior
|
||||
<!--- Tell us what should happen -->
|
||||
|
||||
## Current Behavior
|
||||
<!--- Tell us what happens instead of the expected behavior -->
|
||||
|
||||
## Possible Solution
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
|
||||
## Steps to Reproduce
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Context (Environment)
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
## Detailed Description
|
||||
<!--- Provide a detailed description of the change or addition you are proposing -->
|
||||
|
||||
## Possible Implementation
|
||||
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@ -23,3 +23,18 @@ frontend/qml/ProtonUI/*.qmlc
|
||||
frontend/qml/ProtonUI/fontawesome.ttf
|
||||
frontend/qml/ProtonUI/images
|
||||
frontend/qml/*.qmlc
|
||||
|
||||
# Build files
|
||||
bridge_darwin_*.tgz
|
||||
cmd/Desktop-Bridge/deploy
|
||||
internal/frontend/qt/moc.cpp
|
||||
internal/frontend/qt/moc.go
|
||||
internal/frontend/qt/moc.h
|
||||
internal/frontend/qt/moc_cgo_darwin_darwin_amd64.go
|
||||
internal/frontend/qt/moc_moc.h
|
||||
internal/frontend/qt/rcc.cpp
|
||||
internal/frontend/qt/rcc_cgo_darwin_darwin_amd64.go
|
||||
internal/frontend/rcc.cpp
|
||||
internal/frontend/rcc.qrc
|
||||
internal/frontend/rcc_cgo_darwin_darwin_amd64.go
|
||||
vendor-cache/
|
||||
|
||||
@ -37,6 +37,8 @@ build-ci-image:
|
||||
- ci/*
|
||||
services:
|
||||
- docker:dind
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker info
|
||||
@ -104,7 +106,54 @@ build-linux:
|
||||
script:
|
||||
- make build
|
||||
artifacts:
|
||||
name: "bridge-linux-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
expire_in: 2 week
|
||||
|
||||
build-darwin:
|
||||
stage: build
|
||||
only:
|
||||
- branches
|
||||
before_script:
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOPATH=~/go
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS-bridge
|
||||
script:
|
||||
- make build
|
||||
artifacts:
|
||||
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
expire_in: 2 week
|
||||
|
||||
build-windows:
|
||||
stage: build
|
||||
services:
|
||||
- docker:dind
|
||||
only:
|
||||
- branches
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
script:
|
||||
# We need to install docker because qtdeploy builds for windows inside a docker container.
|
||||
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
|
||||
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
|
||||
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
|
||||
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
|
||||
- go mod download
|
||||
- TARGET_OS=windows make build
|
||||
artifacts:
|
||||
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
expire_in: 2 week
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||
* GCC (linux, windows) or Xcode (macOS)
|
||||
* Windres (windows)
|
||||
* libglvnd and libsecret development files (linux)
|
||||
|
||||
To enable the sending of crash reports using Sentry please set the
|
||||
`main.DSNSentry` value with the client key of your sentry project before build.
|
||||
@ -35,5 +36,5 @@ In order to be able to run following commands please install the development dep
|
||||
|
||||
* `make test` will run all unit tests
|
||||
* `make lint` will lint the whole project
|
||||
* `make -C ./tests test` will run the integration tests
|
||||
* `make -C ./test test` will run the integration tests
|
||||
* `make run` will build Bridge without a GUI and start it in CLI mode
|
||||
|
||||
84
COPYING.md
84
COPYING.md
@ -26,48 +26,48 @@ ProtonMail Bridge includes the following 3rd party software:
|
||||
* [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing)
|
||||
* [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)
|
||||
* [notificator](https://github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
|
||||
* [ishell](https://github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
|
||||
* [readline](https://github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||
* [singleinstance](https://github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
|
||||
* [cascadia](https://github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
||||
* [gocertifi](https://github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE)
|
||||
* [logex](https://github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
|
||||
* [test](https://github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||
* [godog](https://github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
|
||||
* [wincred](https://github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
|
||||
* [credential-helpers](https://github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [imap](https://github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
|
||||
* [imap-appendlimit](https://github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
|
||||
* [imap-idle](https://github.com/emersion/go-imap-idle) | Available under [license](https://github.com/emersion/go-imap-idle/blob/master/LICENSE)
|
||||
* [imap-quota](https://github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
|
||||
* [imap-specialuse](https://github.com/emersion/go-imap-specialuse) | Available under [license](https://github.com/emersion/go-imap-specialuse/blob/master/LICENSE)
|
||||
* [sasl](https://github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
||||
* [smtp](https://github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
||||
* [textwrapper](https://github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||
* [vcard](https://github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||
* [color](https://github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE.md)
|
||||
* [shlex](https://github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/COPYING)
|
||||
* [raven](https://github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE)
|
||||
* [resty](https://github.com/go-resty/resty) | Available under [license](https://github.com/go-resty/resty/blob/master/LICENSE)
|
||||
* [mock](https://github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE)
|
||||
* [cmp](https://github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
|
||||
* [gopherjs](https://github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE)
|
||||
* [multierror](https://github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
|
||||
* [bcrypt](https://github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
|
||||
* [html2text](https://github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
||||
* [enmime](https://github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE)
|
||||
* [osext](https://github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE)
|
||||
* [keychain](https://github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
||||
* [aurora](https://github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
|
||||
* [dns](https://github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
||||
* [uuid](https://github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE)
|
||||
* [jsondiff](https://github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
|
||||
* [logrus](https://github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
|
||||
* [golang](https://github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE)
|
||||
* [testify](https://github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||
* [uuid](https://github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE)
|
||||
* [cli](https://github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE)
|
||||
|
||||
* [BBolt](https://pkg.go.dev/go.etcd.io/bbolt/?tab=doc) | Available under [license](https://pkg.go.dev/go.etcd.io/bbolt?tab=licenses#LICENSE)
|
||||
* [testify.v1](https://gopkg.in/stretchr/testify.v1) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||
|
||||
1127
Changelog.md
1127
Changelog.md
File diff suppressed because it is too large
Load Diff
77
Makefile
77
Makefile
@ -1,17 +1,21 @@
|
||||
export GO111MODULE=on
|
||||
|
||||
# By default, the target OS is the same as the host OS,
|
||||
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
|
||||
GOOS:=$(shell go env GOOS)
|
||||
TARGET_OS?=${GOOS}
|
||||
|
||||
## Build
|
||||
.PHONY: build check-has-go
|
||||
.PHONY: build build-nogui check-has-go
|
||||
|
||||
VERSION?=1.2.6-git
|
||||
BRIDGE_VERSION?=$(shell git describe --abbrev=0 --tags)-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})
|
||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/pkg/constants.,Version=${BRIDGE_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
||||
ifneq "${BUILD_LDFLAGS}" ""
|
||||
GO_LDFLAGS+= ${BUILD_LDFLAGS}
|
||||
endif
|
||||
@ -23,22 +27,26 @@ DEPLOY_DIR:=cmd/Desktop-Bridge/deploy
|
||||
ICO_FILES:=
|
||||
EXE:=$(shell basename ${CURDIR})
|
||||
|
||||
ifeq "${GOOS}" "windows"
|
||||
EXE+=.exe
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
EXE:=${EXE}.exe
|
||||
ICO_FILES:=logo.ico icon.rc icon_windows.syso
|
||||
endif
|
||||
ifeq "${GOOS}" "darwin"
|
||||
ifeq "${TARGET_OS}" "darwin"
|
||||
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
||||
EXE:=${EXE}.app/Contents/MacOS/${EXE}
|
||||
endif
|
||||
EXE_TARGET:=${DEPLOY_DIR}/${GOOS}/${EXE}
|
||||
TGZ_TARGET:=bridge_${GOOS}_${REVISION}.tgz
|
||||
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
|
||||
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
|
||||
|
||||
|
||||
build: ${TGZ_TARGET}
|
||||
|
||||
${TGZ_TARGET}: ${DEPLOY_DIR}/${GOOS}
|
||||
build-nogui:
|
||||
go build ${BUILD_FLAGS_NOGUI} -o Desktop-Bridge cmd/Desktop-Bridge/main.go
|
||||
|
||||
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
|
||||
rm -f $@
|
||||
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${GOOS}
|
||||
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${TARGET_OS}
|
||||
|
||||
${DEPLOY_DIR}/linux: ${EXE_TARGET}
|
||||
cp -pf ./internal/frontend/share/icons/logo.svg ${DEPLOY_DIR}/linux/
|
||||
@ -52,37 +60,48 @@ ${DEPLOY_DIR}/darwin: ${EXE_TARGET}
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}"
|
||||
|
||||
${DEPLOY_DIR}/windows: ${EXE_TARGET}
|
||||
cp ./internal/frontend/share/icons/logo.ico ${DEPLOY_DIR}/windows/
|
||||
cp LICENSE ${DEPLOY_DIR}/windows/
|
||||
|
||||
QT_BUILD_TARGET:=build desktop
|
||||
ifneq "${GOOS}" "${TARGET_OS}"
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
QT_BUILD_TARGET:=-docker build windows_64_shared
|
||||
endif
|
||||
endif
|
||||
|
||||
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
|
||||
rm -rf deploy ${GOOS} ${DEPLOY_DIR}
|
||||
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
|
||||
cp cmd/Desktop-Bridge/main.go .
|
||||
qtdeploy ${BUILD_FLAGS} build desktop
|
||||
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
|
||||
mv deploy cmd/Desktop-Bridge
|
||||
rm -rf ${GOOS} main.go
|
||||
rm -rf ${TARGET_OS} 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 $< $@
|
||||
windres --target=pe-x86-64 -o $@ $<
|
||||
icon_windows.syso: ./internal/frontend/qt/icon_windows.syso
|
||||
cp $^ .
|
||||
|
||||
|
||||
## Rules for therecipe/qt
|
||||
.PHONY: prepare-vendor update-vendor
|
||||
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
|
||||
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_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}
|
||||
|
||||
# The command used to make symlinks is different on windows.
|
||||
# So if the GOOS is windows and we aren't crossbuilding (in which case the host os would still be *nix)
|
||||
# we need to change the LINKCMD to something windowsy.
|
||||
LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV}
|
||||
ifeq "${GOOS}" "windows"
|
||||
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
|
||||
@ -100,7 +119,7 @@ update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated
|
||||
LINTVER:="v1.23.6"
|
||||
LINTVER:="v1.27.0"
|
||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||
|
||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||
@ -118,12 +137,15 @@ install-go-mod-outdated:
|
||||
|
||||
|
||||
## Checks, mocks and docs
|
||||
.PHONY: check-has-go check-license test bench coverage mocks lint updates doc
|
||||
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc
|
||||
check-has-go:
|
||||
@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" {} \;
|
||||
add-license:
|
||||
./utils/missing_license.sh add
|
||||
|
||||
change-copyright-year:
|
||||
./utils/missing_license.sh change-year
|
||||
|
||||
test: gofiles
|
||||
@# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests.
|
||||
@ -134,9 +156,11 @@ test: gofiles
|
||||
./internal/frontend/autoconfig/... \
|
||||
./internal/frontend/cli/... \
|
||||
./internal/imap/... \
|
||||
./internal/metrics/... \
|
||||
./internal/preferences/... \
|
||||
./internal/smtp/... \
|
||||
./internal/store/... \
|
||||
./internal/users/... \
|
||||
./pkg/...
|
||||
|
||||
bench:
|
||||
@ -148,11 +172,17 @@ 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/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
|
||||
|
||||
lint:
|
||||
lint: lint-golang lint-license
|
||||
|
||||
lint-license:
|
||||
./utils/missing_license.sh check
|
||||
|
||||
lint-golang:
|
||||
which golangci-lint || $(MAKE) install-linter
|
||||
golangci-lint run ./...
|
||||
|
||||
@ -206,3 +236,4 @@ clean: clean-frontend-qt
|
||||
rm -rf vendor-cache
|
||||
rm -rf cmd/Desktop-Bridge/deploy
|
||||
rm -f build last.log mem.pprof
|
||||
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso
|
||||
|
||||
35
README.md
35
README.md
@ -6,6 +6,7 @@ For a detailed build information see [BUILDS](./BUILDS.md).
|
||||
For licensing information see [COPYING](./COPYING.md).
|
||||
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
|
||||
## Description
|
||||
ProtonMail Bridge for e-mail clients.
|
||||
|
||||
@ -31,15 +32,16 @@ Windows, Bridge uses native credential managers. On Linux, use
|
||||
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
|
||||
- `BRIDGE_VERSION`: set the bridge app version used during testing or building
|
||||
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
|
||||
- `VERBOSITY`: set log level used during test time and by the makefile.
|
||||
- `VERSION`: set the bridge app version used during testing or building.
|
||||
- `VERBOSITY`: set log level used during test time and by the makefile
|
||||
|
||||
### Integration testing
|
||||
- `TEST_ENV`: set which env to use (fake or live)
|
||||
@ -48,5 +50,34 @@ or
|
||||
- `FEATURES`: set feature dir, file or scenario to test
|
||||
|
||||
|
||||
## Files
|
||||
### Database
|
||||
The database stores metadata necessary for presenting messages and mailboxes to an email client:
|
||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
|
||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
|
||||
|
||||
### Preferences
|
||||
User preferences are stored in json at the following location:
|
||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/prefs.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/prefs.json`
|
||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\prefs.json`
|
||||
|
||||
### IMAP Cache
|
||||
The currently subscribed mailboxes are held in a json file:
|
||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
|
||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
|
||||
|
||||
### Lock file
|
||||
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
|
||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
|
||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
|
||||
|
||||
### TLS Certificate and Key
|
||||
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
|
||||
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
|
||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
|
||||
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`
|
||||
|
||||
|
||||
@ -47,16 +47,17 @@ import (
|
||||
|
||||
"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/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/args"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/getsentry/raven-go"
|
||||
@ -68,45 +69,28 @@ import (
|
||||
// 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]
|
||||
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
|
||||
|
||||
// How many crashes in a row.
|
||||
numberOfCrashes = 0 //nolint[gochecknoglobals]
|
||||
|
||||
// After how many crashes bridge gives up starting.
|
||||
maxAllowedCrashes = 10 //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := raven.SetDSN(DSNSentry); err != nil {
|
||||
if err := raven.SetDSN(constants.DSNSentry); err != nil {
|
||||
log.WithError(err).Errorln("Can not setup sentry DSN")
|
||||
}
|
||||
raven.SetRelease(Revision)
|
||||
bridge.UpdateCurrentUserAgent(Version, runtime.GOOS, "", "")
|
||||
raven.SetRelease(constants.Revision)
|
||||
|
||||
args.FilterProcessSerialNumberFromArgs()
|
||||
filterRestartNumberFromArgs()
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "Protonmail Bridge"
|
||||
app.Version = buildVersion
|
||||
app.Version = constants.BuildVersion
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "log-level, l",
|
||||
@ -135,13 +119,13 @@ func main() {
|
||||
|
||||
// Always log the basic info about current bridge.
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
log.WithField("version", Version).
|
||||
WithField("revision", Revision).
|
||||
log.WithField("version", constants.Version).
|
||||
WithField("revision", constants.Revision).
|
||||
WithField("runtime", runtime.GOOS).
|
||||
WithField("build", BuildTime).
|
||||
WithField("build", constants.BuildTime).
|
||||
WithField("args", os.Args).
|
||||
WithField("appLong", app.Name).
|
||||
WithField("appShort", AppShortName).
|
||||
WithField("appShort", constants.AppShortName).
|
||||
Info("Run app")
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Error("Program exited with error: ", err)
|
||||
@ -174,7 +158,7 @@ func (ph *panicHandler) HandlePanic() {
|
||||
// IMPORTANT: ***Read the comments before CHANGING the order ***
|
||||
func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
// We need to have config instance to setup a logs, panic handler, etc ...
|
||||
cfg := config.New(AppShortName, Version, Revision, cacheVersion)
|
||||
cfg := config.New(constants.AppShortName, constants.Version, constants.Revision, cacheVersion)
|
||||
|
||||
// We want to know about any problem. Our PanicHandler calls sentry which is
|
||||
// not dependent on anything else. If that fails, it tries to create crash
|
||||
@ -208,7 +192,16 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
|
||||
// It's safe to get version JSON file even when other instance is running.
|
||||
// (thus we put it before check of presence of other Bridge instance).
|
||||
updates := updates.New(AppShortName, Version, Revision, BuildTime, bridge.ReleaseNotes, bridge.ReleaseFixedBugs, cfg.GetUpdateDir())
|
||||
updates := updates.New(
|
||||
constants.AppShortName,
|
||||
constants.Version,
|
||||
constants.Revision,
|
||||
constants.BuildTime,
|
||||
bridge.ReleaseNotes,
|
||||
bridge.ReleaseFixedBugs,
|
||||
cfg.GetUpdateDir(),
|
||||
)
|
||||
|
||||
if dir := context.GlobalString("version-json"); dir != "" {
|
||||
generateVersionFiles(updates, dir)
|
||||
return nil
|
||||
@ -268,14 +261,19 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
eventListener := listener.New()
|
||||
events.SetupEvents(eventListener)
|
||||
|
||||
credentialsStore, credentialsError := credentials.NewStore()
|
||||
credentialsStore, credentialsError := credentials.NewStore("bridge")
|
||||
if credentialsError != nil {
|
||||
log.Error("Could not get credentials store: ", credentialsError)
|
||||
}
|
||||
|
||||
pmapiClientFactory := pmapifactory.New(cfg, eventListener)
|
||||
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
|
||||
|
||||
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, Version, pmapiClientFactory, credentialsStore)
|
||||
// Different build types have different roundtrippers (e.g. we want to enable
|
||||
// TLS fingerprint checks in production builds). GetRoundTripper has a different
|
||||
// implementation depending on whether build flag pmapi_prod is used or not.
|
||||
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
|
||||
|
||||
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, cm, credentialsStore)
|
||||
imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance)
|
||||
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
|
||||
|
||||
@ -321,7 +319,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
}
|
||||
|
||||
showWindowOnStart := !context.GlobalBool("no-window")
|
||||
frontend := frontend.New(Version, buildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
|
||||
frontend := frontend.New(constants.Version, constants.BuildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
|
||||
|
||||
// Last part is to start everything.
|
||||
log.Debug("Starting frontend...")
|
||||
@ -341,7 +339,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
// It will happen only when c10/prefs.json exists and c11/prefs.json not.
|
||||
// No configuration changed between c10 and c11 versions.
|
||||
func migratePreferencesFromC10(cfg *config.Config) {
|
||||
pref10Path := config.New(AppShortName, Version, Revision, "c10").GetPreferencesPath()
|
||||
pref10Path := config.New(constants.AppShortName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
|
||||
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
|
||||
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
|
||||
return
|
||||
@ -359,7 +357,7 @@ func migratePreferencesFromC10(cfg *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(pref11Path, data, 0644)
|
||||
err = ioutil.WriteFile(pref11Path, data, 0600)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Problem to migrate preferences")
|
||||
return
|
||||
|
||||
32
go.mod
32
go.mod
@ -5,21 +5,20 @@ 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/docker/docker-credential-helpers v0.6.3
|
||||
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
|
||||
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
|
||||
github.com/ProtonMail/go-appdir v1.0.0
|
||||
github.com/ProtonMail/go-appdir v1.1.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-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
|
||||
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.0.1
|
||||
github.com/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
|
||||
@ -28,11 +27,13 @@ require (
|
||||
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 v1.0.6-0.20200708083111-011063d6c9df
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
|
||||
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
|
||||
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
|
||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
|
||||
@ -57,22 +58,17 @@ require (
|
||||
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
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
|
||||
)
|
||||
|
||||
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/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||
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
|
||||
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c
|
||||
)
|
||||
|
||||
95
go.sum
95
go.sum
@ -3,30 +3,26 @@ github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9Ww
|
||||
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/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
|
||||
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
|
||||
github.com/ProtonMail/go-appdir v1.1.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
|
||||
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h1:YsSJ/mvZFYydQm/hRrt8R8UtgETixN2y3LK98f5LT60=
|
||||
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-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-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
|
||||
github.com/ProtonMail/go-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/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
|
||||
github.com/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=
|
||||
@ -35,7 +31,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:m
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
|
||||
github.com/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=
|
||||
@ -46,33 +41,29 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWs
|
||||
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/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
|
||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||
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 v1.0.6-0.20200708083111-011063d6c9df h1:Vlwnsd5P5s+ek+wzEXbZ/g9tUBndVQAb3E1/+/ya3UQ=
|
||||
github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
|
||||
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
|
||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
|
||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
|
||||
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe h1:2R2XpJkmbyy7PcSjnCPOnNfu+GuRzgWR9U2+j/d1O+0=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20161227193600-1e6dc73ac8fe h1:WeXweyFnbM2DQx0wxHkJKXYXwXpApopIeAjDTipW5Z4=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20161227193600-1e6dc73ac8fe/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
||||
github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac=
|
||||
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
|
||||
@ -89,10 +80,6 @@ github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+j
|
||||
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=
|
||||
@ -106,8 +93,6 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
|
||||
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=
|
||||
@ -117,20 +102,15 @@ github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uia
|
||||
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/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
|
||||
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/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=
|
||||
@ -138,10 +118,8 @@ github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/
|
||||
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=
|
||||
@ -175,24 +153,20 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
|
||||
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/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
|
||||
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
|
||||
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
|
||||
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
|
||||
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/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=
|
||||
@ -203,18 +177,17 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-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/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/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=
|
||||
@ -224,13 +197,9 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV
|
||||
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=
|
||||
|
||||
@ -32,10 +32,11 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
log = config.GetLogEntry("api") //nolint[gochecknoglobals]
|
||||
log = logrus.WithField("pkg", "api") //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
type apiServer struct {
|
||||
|
||||
@ -15,55 +15,30 @@
|
||||
// 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 provides core functionality of Bridge app.
|
||||
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/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/internal/users"
|
||||
|
||||
"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]
|
||||
log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
// Bridge is a struct handling users.
|
||||
type Bridge struct {
|
||||
config Configer
|
||||
*users.Users
|
||||
|
||||
pref PreferenceProvider
|
||||
panicHandler PanicHandler
|
||||
events listener.Listener
|
||||
version string
|
||||
pmapiClientFactory PMAPIProviderFactory
|
||||
credStorer CredentialsStorer
|
||||
storeCache *store.Cache
|
||||
|
||||
// users is a list of accounts that have been added to bridge.
|
||||
// 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
|
||||
clientManager users.ClientManager
|
||||
|
||||
userAgentClientName string
|
||||
userAgentClientVersion string
|
||||
@ -73,46 +48,28 @@ type Bridge struct {
|
||||
func New(
|
||||
config Configer,
|
||||
pref PreferenceProvider,
|
||||
panicHandler PanicHandler,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
version string,
|
||||
pmapiClientFactory PMAPIProviderFactory,
|
||||
credStorer CredentialsStorer,
|
||||
clientManager users.ClientManager,
|
||||
credStorer users.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.
|
||||
// Allow DoH before starting the app if the user has previously set this setting.
|
||||
// This allows us to start even if protonmail is blocked.
|
||||
if pref.GetBool(preferences.AllowProxyKey) {
|
||||
AllowDoH()
|
||||
clientManager.AllowProxy()
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
b.watchBridgeOutdated()
|
||||
}()
|
||||
storeFactory := newStoreFactory(config, panicHandler, clientManager, eventListener)
|
||||
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory)
|
||||
b := &Bridge{
|
||||
Users: u,
|
||||
|
||||
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")
|
||||
pref: pref,
|
||||
clientManager: clientManager,
|
||||
}
|
||||
|
||||
if pref.GetBool(preferences.FirstStartKey) {
|
||||
b.SendMetric(m.New(m.Setup, m.FirstStart, m.Label(version)))
|
||||
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
|
||||
}
|
||||
|
||||
go b.heartbeat()
|
||||
@ -122,325 +79,22 @@ func New(
|
||||
|
||||
// heartbeat sends a heartbeat signal once a day.
|
||||
func (b *Bridge) heartbeat() {
|
||||
for range time.NewTicker(1 * time.Hour).C {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
|
||||
for range ticker.C {
|
||||
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
nextTime := time.Unix(next, 0)
|
||||
if time.Now().After(nextTime) {
|
||||
b.SendMetric(m.New(m.Heartbeat, m.Daily, m.NoLabel))
|
||||
b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
|
||||
nextTime = nextTime.Add(24 * time.Hour)
|
||||
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
|
||||
@ -455,56 +109,40 @@ func (b *Bridge) GetCurrentClient() string {
|
||||
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
|
||||
b.userAgentClientName = clientName
|
||||
b.userAgentClientVersion = clientVersion
|
||||
b.updateCurrentUserAgent()
|
||||
b.updateUserAgent()
|
||||
}
|
||||
|
||||
// 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()
|
||||
b.updateUserAgent()
|
||||
}
|
||||
|
||||
// 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")
|
||||
func (b *Bridge) updateUserAgent() {
|
||||
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
|
||||
}
|
||||
|
||||
// ReportBug reports a new bug from the user.
|
||||
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
|
||||
c := b.clientManager.GetAnonymousClient()
|
||||
defer c.Logout()
|
||||
|
||||
title := "[Bridge] Bug"
|
||||
if err := c.ReportBugWithEmailClient(
|
||||
osType,
|
||||
osVersion,
|
||||
title,
|
||||
description,
|
||||
accountName,
|
||||
address,
|
||||
emailClient,
|
||||
); err != nil {
|
||||
log.Error("Reporting bug failed: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return b.idleUpdates
|
||||
}
|
||||
log.Info("Bug successfully reported")
|
||||
|
||||
// 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!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,233 +0,0 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBridgeFinishLoginBadPassword(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
// Init bridge with no user from keychain.
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
||||
|
||||
// Set up mocks for FinishLogin.
|
||||
err := errors.New("bad password")
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, err)
|
||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
||||
|
||||
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
|
||||
}
|
||||
|
||||
func TestBridgeFinishLoginUpgradeApplication(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
// Init bridge with no user from keychain.
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
||||
|
||||
// Set up mocks for FinishLogin.
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, pmapi.ErrUpgradeApplication)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
|
||||
err := errors.New("Cannot logout when upgrade needed")
|
||||
m.pmapiClient.EXPECT().Logout().Return(err)
|
||||
|
||||
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
|
||||
}
|
||||
|
||||
func refreshWithToken(token string) *pmapi.Auth {
|
||||
return &pmapi.Auth{
|
||||
RefreshToken: token,
|
||||
KeySalt: "", // No salting in tests.
|
||||
}
|
||||
}
|
||||
|
||||
func credentialsWithToken(token string) *credentials.Credentials {
|
||||
tmp := &credentials.Credentials{}
|
||||
*tmp = *testCredentials
|
||||
tmp.APIToken = token
|
||||
return tmp
|
||||
}
|
||||
|
||||
func TestBridgeFinishLoginNewUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
// Bridge finds no users in the keychain.
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
||||
|
||||
// Get user to be able to setup new client with proper userID.
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
||||
|
||||
// Setup of new client.
|
||||
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
|
||||
// Set up mocks for authorising the new user (in user.init).
|
||||
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
|
||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil).Times(2)
|
||||
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
||||
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
|
||||
|
||||
// Set up mocks for creating the user's store (in store.New).
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
||||
|
||||
// Emit event for new user and send metrics.
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.NewUser), string(metrics.NoLabel))
|
||||
|
||||
// Set up mocks for starting the store's event loop (in store.New).
|
||||
// The event loop runs in another goroutine so this might happen at any time.
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
||||
|
||||
// Set up mocks for performing the initial store sync.
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
||||
|
||||
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
|
||||
}
|
||||
|
||||
func TestBridgeFinishLoginExistingUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
loggedOutCreds := *testCredentials
|
||||
loggedOutCreds.APIToken = ""
|
||||
loggedOutCreds.MailboxPassword = ""
|
||||
|
||||
// Bridge finds one logged out user in the keychain.
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
||||
// New user
|
||||
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
|
||||
// Init user
|
||||
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrInvalidToken)
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
||||
|
||||
// Get user to be able to setup new client with proper userID.
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
||||
|
||||
// Setup of new client.
|
||||
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
|
||||
// Set up mocks for authorising the new user (in user.init).
|
||||
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
|
||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil)
|
||||
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
||||
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
|
||||
|
||||
// Set up mocks for creating the user's store (in store.New).
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
||||
|
||||
// Reload account list in GUI.
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
|
||||
// Set up mocks for starting the store's event loop (in store.New)
|
||||
// The event loop runs in another goroutine so this might happen at any time.
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
||||
|
||||
// Set up mocks for performing the initial store sync.
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
||||
|
||||
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
|
||||
}
|
||||
|
||||
func TestBridgeDoubleLogin(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
// Firstly, start bridge with existing user...
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
||||
|
||||
bridge := testNewBridge(t, m)
|
||||
defer cleanUpBridgeUserData(bridge)
|
||||
|
||||
// Then, try to log in again...
|
||||
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
|
||||
m.pmapiClient.EXPECT().Logout()
|
||||
|
||||
_, err := bridge.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
|
||||
assert.Equal(t, "user is already logged in", err.Error())
|
||||
}
|
||||
|
||||
func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) {
|
||||
bridge := testNewBridge(t, m)
|
||||
defer cleanUpBridgeUserData(bridge)
|
||||
|
||||
user, err := bridge.FinishLogin(m.pmapiClient, auth, mailboxPassword)
|
||||
|
||||
waitForEvents()
|
||||
|
||||
assert.Equal(t, expectedErr, err)
|
||||
|
||||
if expectedUserID != "" {
|
||||
assert.Equal(t, expectedUserID, user.ID())
|
||||
assert.Equal(t, 1, len(bridge.users))
|
||||
assert.Equal(t, expectedUserID, bridge.users[0].ID())
|
||||
} else {
|
||||
assert.Equal(t, (*User)(nil), user)
|
||||
assert.Equal(t, 0, len(bridge.users))
|
||||
}
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewBridgeNoKeychain(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain"))
|
||||
|
||||
checkBridgeNew(t, m, []*credentials.Credentials{})
|
||||
}
|
||||
|
||||
func TestNewBridgeWithoutUsersInCredentialsStore(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
||||
|
||||
checkBridgeNew(t, m, []*credentials.Credentials{})
|
||||
}
|
||||
|
||||
func TestNewBridgeWithDisconnectedUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
||||
|
||||
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
||||
}
|
||||
|
||||
func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token"))
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
||||
m.pmapiClient.EXPECT().SetAuths(nil)
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
||||
}
|
||||
|
||||
func TestNewBridgeWithConnectedUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
||||
|
||||
// Set up mocks for store initialisation for the authorized user.
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
||||
|
||||
checkBridgeNew(t, m, []*credentials.Credentials{testCredentials})
|
||||
}
|
||||
|
||||
// Tests two users with different states and checks also the order from
|
||||
// credentials store is kept also in array of Bridge users.
|
||||
func TestNewBridgeWithUsers(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
||||
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user", "user"}, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
||||
|
||||
// Set up mocks for store initialisation for the unauth user.
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
||||
|
||||
// Set up mocks for store initialisation for the authorized user.
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
||||
|
||||
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
|
||||
}
|
||||
|
||||
func TestNewBridgeFirstStart(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(true)
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
||||
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.FirstStart), gomock.Any())
|
||||
|
||||
testNewBridge(t, m)
|
||||
}
|
||||
|
||||
func checkBridgeNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
|
||||
bridge := testNewBridge(t, m)
|
||||
defer cleanUpBridgeUserData(bridge)
|
||||
|
||||
assert.Equal(m.t, len(expectedCredentials), len(bridge.GetUsers()))
|
||||
|
||||
credentials := []*credentials.Credentials{}
|
||||
for _, user := range bridge.users {
|
||||
credentials = append(credentials, user.creds)
|
||||
}
|
||||
|
||||
assert.Equal(m.t, expectedCredentials, credentials)
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNoUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
checkBridgeGetUser(t, m, "nouser", -1, "user nouser not found")
|
||||
}
|
||||
|
||||
func TestGetUserByID(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
checkBridgeGetUser(t, m, "user", 0, "")
|
||||
checkBridgeGetUser(t, m, "users", 1, "")
|
||||
}
|
||||
|
||||
func TestGetUserByName(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
checkBridgeGetUser(t, m, "username", 0, "")
|
||||
checkBridgeGetUser(t, m, "usersname", 1, "")
|
||||
}
|
||||
|
||||
func TestGetUserByEmail(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
checkBridgeGetUser(t, m, "user@pm.me", 0, "")
|
||||
checkBridgeGetUser(t, m, "users@pm.me", 1, "")
|
||||
checkBridgeGetUser(t, m, "anotheruser@pm.me", 1, "")
|
||||
checkBridgeGetUser(t, m, "alsouser@pm.me", 1, "")
|
||||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
bridge := testNewBridgeWithUsers(t, m)
|
||||
defer cleanUpBridgeUserData(bridge)
|
||||
|
||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
||||
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
m.credentialsStore.EXPECT().Delete("user").Return(nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
err := bridge.DeleteUser("user", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(bridge.users))
|
||||
}
|
||||
|
||||
// Even when logout fails, delete is done.
|
||||
func TestDeleteUserWithFailingLogout(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
bridge := testNewBridgeWithUsers(t, m)
|
||||
defer cleanUpBridgeUserData(bridge)
|
||||
|
||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
||||
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed"))
|
||||
m.credentialsStore.EXPECT().Delete("user").Return(nil).Times(2)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
err := bridge.DeleteUser("user", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(bridge.users))
|
||||
}
|
||||
|
||||
func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
|
||||
bridge := testNewBridgeWithUsers(t, m)
|
||||
defer cleanUpBridgeUserData(bridge)
|
||||
|
||||
user, err := bridge.GetUser(query)
|
||||
waitForEvents()
|
||||
|
||||
if expectedError != "" {
|
||||
assert.Equal(m.t, expectedError, err.Error())
|
||||
} else {
|
||||
assert.NoError(m.t, err)
|
||||
}
|
||||
|
||||
var expectedUser *User
|
||||
if index >= 0 {
|
||||
expectedUser = bridge.users[index]
|
||||
}
|
||||
|
||||
assert.Equal(m.t, expectedUser, user)
|
||||
}
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at Thu Apr 9 13:39:29 CEST 2020. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Wed 29 Jul 2020 06:44:08 AM CEST. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-imap-quota;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/danieljoos/wincred;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
|
||||
@ -1,923 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/internal/bridge (interfaces: Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
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)
|
||||
}
|
||||
@ -15,20 +15,17 @@
|
||||
// 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.
|
||||
// Code generated by ./release-notes.sh at Wed 29 Jul 2020 07:07:28 AM CEST. 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 ReleaseNotes = `• Improvements to Alternative Routing: Version two of this feature is now more resilient to unstable internet connections, which results in a smoother experience using this feature. Also includes fixes to previous implementation of Alternative Routing when first starting the application or when turning it off.
|
||||
• Email parsing improvements: Improved detection of email encodings embedded in html/xml in addition to message header; add a fallback option if encoding is not specified and decoding as UTF8 fails (ISO-8859-1) ; tweaked logic of parsing "References" header.
|
||||
• User interaction improvements: Some smaller improvements in specific cases to make the interaction with Proton Bridge clearer for the user
|
||||
• Code updates & maintenance: Migrated to GopenPGP v2, updates to GoIMAPv1, increased bbolt version to 1.3.5 and various improvements regarding extensibility and maintainability for upcoming work.
|
||||
• General stability improvements: Improvements to the behavior of the application under various unstable internet conditions.
|
||||
`
|
||||
|
||||
const ReleaseFixedBugs = `• Fixed rare case of sending the same message multiple times in Outlook
|
||||
• Fixed bug in macOS update process; available from next update
|
||||
const ReleaseFixedBugs = `• Fixed a slew of smaller bugs and some conditions which could cause the application to crash.
|
||||
• The full changelog can be found at https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
|
||||
`
|
||||
|
||||
69
internal/bridge/store_factory.go
Normal file
69
internal/bridge/store_factory.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
)
|
||||
|
||||
type storeFactory struct {
|
||||
config StoreFactoryConfiger
|
||||
panicHandler users.PanicHandler
|
||||
clientManager users.ClientManager
|
||||
eventListener listener.Listener
|
||||
storeCache *store.Cache
|
||||
}
|
||||
|
||||
func newStoreFactory(
|
||||
config StoreFactoryConfiger,
|
||||
panicHandler users.PanicHandler,
|
||||
clientManager users.ClientManager,
|
||||
eventListener listener.Listener,
|
||||
) *storeFactory {
|
||||
return &storeFactory{
|
||||
config: config,
|
||||
panicHandler: panicHandler,
|
||||
clientManager: clientManager,
|
||||
eventListener: eventListener,
|
||||
storeCache: store.NewCache(config.GetIMAPCachePath()),
|
||||
}
|
||||
}
|
||||
|
||||
// New creates new store for given user.
|
||||
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
|
||||
storePath := getUserStorePath(f.config.GetDBDir(), user.ID())
|
||||
return store.New(f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache)
|
||||
}
|
||||
|
||||
// Remove removes all store files for given user.
|
||||
func (f *storeFactory) Remove(userID string) error {
|
||||
storePath := getUserStorePath(f.config.GetDBDir(), userID)
|
||||
return store.RemoveStore(f.storeCache, storePath, userID)
|
||||
}
|
||||
|
||||
// getUserStorePath returns the file path of the store database for the given userID.
|
||||
func getUserStorePath(storeDir string, userID string) (path string) {
|
||||
fileName := fmt.Sprintf("mailbox-%v.db", userID)
|
||||
return filepath.Join(storeDir, fileName)
|
||||
}
|
||||
@ -17,19 +17,16 @@
|
||||
|
||||
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
|
||||
)
|
||||
import "github.com/ProtonMail/proton-bridge/internal/users"
|
||||
|
||||
type Configer interface {
|
||||
ClearData() error
|
||||
users.Configer
|
||||
StoreFactoryConfiger
|
||||
}
|
||||
|
||||
type StoreFactoryConfiger interface {
|
||||
GetDBDir() string
|
||||
GetIMAPCachePath() string
|
||||
GetAPIConfig() *pmapi.ClientConfig
|
||||
}
|
||||
|
||||
type PreferenceProvider interface {
|
||||
@ -38,68 +35,3 @@ type PreferenceProvider interface {
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,188 +0,0 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
a "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewUserNoCredentialsStore(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("fail"))
|
||||
|
||||
_, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
||||
a.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewUserBridgeOutdated(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil).AnyTimes()
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrUpgradeApplication).AnyTimes()
|
||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
||||
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "").AnyTimes()
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrUpgradeApplication)
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
||||
|
||||
checkNewUser(m)
|
||||
}
|
||||
|
||||
func TestNewUserNoInternetConnection(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
|
||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
||||
m.eventListener.EXPECT().Emit(events.InternetOffEvent, "").AnyTimes()
|
||||
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil)
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrAPINotReachable)
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
|
||||
|
||||
checkNewUser(m)
|
||||
}
|
||||
|
||||
func TestNewUserAuthRefreshFails(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token")).AnyTimes()
|
||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
||||
m.pmapiClient.EXPECT().SetAuths(nil)
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
checkNewUserDisconnected(m)
|
||||
}
|
||||
|
||||
func TestNewUserUnlockFails(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
|
||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, errors.New("bad password"))
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
||||
m.pmapiClient.EXPECT().SetAuths(nil)
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
checkNewUserDisconnected(m)
|
||||
}
|
||||
|
||||
func TestNewUserUnlockAddressesFails(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
|
||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(errors.New("bad password"))
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.pmapiClient.EXPECT().Logout().Return(nil)
|
||||
m.pmapiClient.EXPECT().SetAuths(nil)
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
checkNewUserDisconnected(m)
|
||||
}
|
||||
|
||||
func TestNewUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
||||
|
||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
|
||||
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
||||
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
|
||||
|
||||
checkNewUser(m)
|
||||
}
|
||||
|
||||
func checkNewUser(m mocks) {
|
||||
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
_ = user.init(nil, m.pmapiClient)
|
||||
|
||||
waitForEvents()
|
||||
|
||||
a.Equal(m.t, testCredentials, user.creds)
|
||||
}
|
||||
|
||||
func checkNewUserDisconnected(m mocks) {
|
||||
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
_ = user.init(nil, m.pmapiClient)
|
||||
|
||||
waitForEvents()
|
||||
|
||||
a.Equal(m.t, testCredentialsDisconnected, user.creds)
|
||||
}
|
||||
|
||||
func _TestUserEventRefreshUpdatesAddresses(t *testing.T) { // nolint[funlen]
|
||||
a.Fail(t, "not implemented")
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testNewUser(m mocks) *User {
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
||||
|
||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
|
||||
|
||||
// Expectations for initial sync (when loading existing user from credentials store).
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
|
||||
|
||||
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
||||
assert.NoError(m.t, err)
|
||||
|
||||
err = user.init(nil, m.pmapiClient)
|
||||
assert.NoError(m.t, err)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func testNewUserForLogout(m mocks) *User {
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
|
||||
|
||||
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
|
||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
|
||||
|
||||
// These may or may not be hit depending on how fast the log out happens.
|
||||
m.pmapiClient.EXPECT().SetAuths(nil).AnyTimes()
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil).AnyTimes()
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}).AnyTimes()
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
|
||||
|
||||
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
|
||||
assert.NoError(m.t, err)
|
||||
|
||||
err = user.init(nil, m.pmapiClient)
|
||||
assert.NoError(m.t, err)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func cleanUpUserData(u *User) {
|
||||
_ = u.clearStore()
|
||||
}
|
||||
|
||||
func _TestNeverLongStorePath(t *testing.T) { // nolint[unused]
|
||||
assert.Fail(t, "not implemented")
|
||||
}
|
||||
|
||||
func TestClearStoreWithStore(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
user := testNewUserForLogout(m)
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
require.Nil(t, user.store.Close())
|
||||
user.store = nil
|
||||
assert.Nil(t, user.clearStore())
|
||||
}
|
||||
|
||||
func TestClearStoreWithoutStore(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
user := testNewUserForLogout(m)
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
assert.NotNil(t, user.store)
|
||||
assert.Nil(t, user.clearStore())
|
||||
}
|
||||
@ -28,9 +28,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mobileconfig "github.com/ProtonMail/go-apple-mobileconfig"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
mobileconfig "github.com/ProtonMail/go-apple-mobileconfig"
|
||||
)
|
||||
|
||||
func init() { //nolint[gochecknoinit]
|
||||
|
||||
@ -26,10 +26,11 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
log = config.GetLogEntry("frontend/cli") //nolint[gochecknoglobals]
|
||||
log = logrus.WithField("pkg", "frontend/cli") //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
type frontendCLI struct {
|
||||
|
||||
@ -22,9 +22,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@ -42,7 +40,7 @@ func (f *frontendCLI) restart(c *ishell.Context) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
||||
if connection.CheckInternetConnection() == nil {
|
||||
if f.bridge.CheckConnection() == nil {
|
||||
f.Println("Internet connection is available.")
|
||||
} else {
|
||||
f.Println("Can not contact server please check you internet connection.")
|
||||
@ -135,13 +133,13 @@ func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
|
||||
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
|
||||
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
|
||||
f.preferences.SetBool(preferences.AllowProxyKey, false)
|
||||
bridge.DisallowDoH()
|
||||
f.bridge.DisallowProxy()
|
||||
}
|
||||
} else {
|
||||
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
|
||||
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
|
||||
f.preferences.SetBool(preferences.AllowProxyKey, true)
|
||||
bridge.AllowDoH()
|
||||
f.bridge.AllowProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,10 +26,11 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
log = config.GetLogEntry("frontend") // nolint[unused]
|
||||
log = logrus.WithField("pkg", "frontend") // nolint[unused]
|
||||
)
|
||||
|
||||
// Frontend is an interface to be implemented by each frontend type (cli, gui, html).
|
||||
|
||||
@ -36,6 +36,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
|
||||
@ -44,11 +45,11 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/useragent"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
//"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
@ -58,7 +59,7 @@ import (
|
||||
"github.com/therecipe/qt/widgets"
|
||||
)
|
||||
|
||||
var log = config.GetLogEntry("frontend-qt")
|
||||
var log = logrus.WithField("pkg", "frontend-qt")
|
||||
var accountMutex = &sync.Mutex{}
|
||||
|
||||
// API between Bridge and Qt.
|
||||
@ -85,7 +86,7 @@ type FrontendQt struct {
|
||||
programName string // Program name (shown in taskbar).
|
||||
programVer string // Program version (shown in help).
|
||||
|
||||
authClient bridge.PMAPIProvider
|
||||
authClient pmapi.Client
|
||||
|
||||
auth *pmapi.Auth
|
||||
|
||||
@ -527,11 +528,11 @@ func (s *FrontendQt) toggleAllowProxy() {
|
||||
|
||||
if s.preferences.GetBool(preferences.AllowProxyKey) {
|
||||
s.preferences.SetBool(preferences.AllowProxyKey, false)
|
||||
bridge.DisallowDoH()
|
||||
s.bridge.DisallowProxy()
|
||||
s.Qml.SetIsProxyAllowed(false)
|
||||
} else {
|
||||
s.preferences.SetBool(preferences.AllowProxyKey, true)
|
||||
bridge.AllowDoH()
|
||||
s.bridge.AllowProxy()
|
||||
s.Qml.SetIsProxyAllowed(true)
|
||||
}
|
||||
}
|
||||
@ -568,7 +569,7 @@ func (s *FrontendQt) isSMTPSTARTTLS() bool {
|
||||
}
|
||||
|
||||
func (s *FrontendQt) checkInternet() {
|
||||
s.Qml.SetConnectionStatus(IsInternetAvailable())
|
||||
s.Qml.SetConnectionStatus(s.bridge.CheckConnection() == nil)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) switchAddressModeUser(iAccount int) {
|
||||
|
||||
@ -26,9 +26,10 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var log = config.GetLogEntry("frontend-nogui") //nolint[gochecknoglobals]
|
||||
var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
|
||||
|
||||
type FrontendHeadless struct{}
|
||||
|
||||
|
||||
@ -25,7 +25,6 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/connection"
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
@ -64,10 +63,6 @@ func PauseLong() {
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
func IsInternetAvailable() bool {
|
||||
return connection.CheckInternetConnection() == nil
|
||||
}
|
||||
|
||||
// FIXME: Not working in test...
|
||||
func WaitForEnter() {
|
||||
log.Print("Press 'Enter' to continue...")
|
||||
|
||||
17
internal/frontend/share/icons/export.sh
Executable file → Normal file
17
internal/frontend/share/icons/export.sh
Executable file → Normal file
@ -1,5 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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/>.
|
||||
|
||||
# create bitmaps
|
||||
for shape in rounded rectangle
|
||||
do
|
||||
|
||||
@ -20,7 +20,7 @@ package types
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
)
|
||||
|
||||
@ -45,13 +45,16 @@ type NoEncConfirmator interface {
|
||||
type Bridger interface {
|
||||
GetCurrentClient() string
|
||||
SetCurrentOS(os string)
|
||||
Login(username, password string) (bridge.PMAPIProvider, *pmapi.Auth, error)
|
||||
FinishLogin(client bridge.PMAPIProvider, auth *pmapi.Auth, mailboxPassword string) (BridgeUser, error)
|
||||
Login(username, password string) (pmapi.Client, *pmapi.Auth, error)
|
||||
FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (BridgeUser, error)
|
||||
GetUsers() []BridgeUser
|
||||
GetUser(query string) (BridgeUser, error)
|
||||
DeleteUser(userID string, clearCache bool) error
|
||||
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
|
||||
ClearData() error
|
||||
AllowProxy()
|
||||
DisallowProxy()
|
||||
CheckConnection() error
|
||||
}
|
||||
|
||||
// BridgeUser is an interface of user needed by frontend.
|
||||
@ -78,7 +81,7 @@ func NewBridgeWrap(bridge *bridge.Bridge) *bridgeWrap { //nolint[golint]
|
||||
return &bridgeWrap{Bridge: bridge}
|
||||
}
|
||||
|
||||
func (b *bridgeWrap) FinishLogin(client bridge.PMAPIProvider, auth *pmapi.Auth, mailboxPassword string) (BridgeUser, error) {
|
||||
func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (BridgeUser, error) {
|
||||
return b.Bridge.FinishLogin(client, auth, mailboxPassword)
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/emersion/go-imap"
|
||||
goIMAPBackend "github.com/emersion/go-imap/backend"
|
||||
)
|
||||
|
||||
@ -37,7 +38,7 @@ type panicHandler interface {
|
||||
type imapBackend struct {
|
||||
panicHandler panicHandler
|
||||
bridge bridger
|
||||
updates chan interface{}
|
||||
updates chan goIMAPBackend.Update
|
||||
eventListener listener.Listener
|
||||
|
||||
users map[string]*imapUser
|
||||
@ -79,7 +80,7 @@ func newIMAPBackend(
|
||||
return &imapBackend{
|
||||
panicHandler: panicHandler,
|
||||
bridge: bridge,
|
||||
updates: make(chan interface{}),
|
||||
updates: make(chan goIMAPBackend.Update),
|
||||
eventListener: eventListener,
|
||||
|
||||
users: map[string]*imapUser{},
|
||||
@ -150,7 +151,7 @@ func (ib *imapBackend) deleteUser(address string) {
|
||||
}
|
||||
|
||||
// Login authenticates a user.
|
||||
func (ib *imapBackend) Login(username, password string) (goIMAPBackend.User, error) {
|
||||
func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMAPBackend.User, error) {
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer ib.panicHandler.HandlePanic()
|
||||
|
||||
@ -179,7 +180,7 @@ func (ib *imapBackend) Login(username, password string) (goIMAPBackend.User, err
|
||||
}
|
||||
|
||||
// Updates returns a channel of updates for IMAP IDLE extension.
|
||||
func (ib *imapBackend) Updates() <-chan interface{} {
|
||||
func (ib *imapBackend) Updates() <-chan goIMAPBackend.Update {
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer ib.panicHandler.HandlePanic()
|
||||
|
||||
|
||||
@ -19,6 +19,8 @@ package imap
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type configProvider interface {
|
||||
@ -43,7 +45,7 @@ type bridgeUser interface {
|
||||
Logout() error
|
||||
CloseConnection(address string)
|
||||
GetStore() storeUserProvider
|
||||
GetTemporaryPMAPIClient() bridge.PMAPIProvider
|
||||
GetTemporaryPMAPIClient() pmapi.Client
|
||||
}
|
||||
|
||||
type bridgeWrap struct {
|
||||
@ -66,10 +68,10 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
|
||||
}
|
||||
|
||||
type bridgeUserWrap struct {
|
||||
*bridge.User
|
||||
*users.User
|
||||
}
|
||||
|
||||
func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap {
|
||||
func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
|
||||
return &bridgeUserWrap{User: bridgeUser}
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
package imap
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
const (
|
||||
fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP).
|
||||
@ -31,5 +31,5 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
log = config.GetLogEntry("imap") //nolint[gochecknoglobals]
|
||||
log = logrus.WithField("pkg", "imap") //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
@ -106,7 +106,7 @@ func (im *imapMailbox) getFlags() []string {
|
||||
//
|
||||
// It always returns the state of DB (which could be different to server status).
|
||||
// Additionally it checks that all stored numbers are same as in DB and polls events if needed.
|
||||
func (im *imapMailbox) Status(items []string) (*imap.MailboxStatus, error) {
|
||||
func (im *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
@ -125,11 +125,17 @@ func (im *imapMailbox) Status(items []string) (*imap.MailboxStatus, error) {
|
||||
message.ThunderbirdNonJunkFlag,
|
||||
}
|
||||
|
||||
dbTotal, dbUnread, err := im.storeMailbox.GetCounts()
|
||||
l.Debugln("DB: total", dbTotal, "unread", dbUnread, "err", err)
|
||||
dbTotal, dbUnread, dbUnreadSeqNum, err := im.storeMailbox.GetCounts()
|
||||
l.WithFields(logrus.Fields{
|
||||
"total": dbTotal,
|
||||
"unread": dbUnread,
|
||||
"unreadSeqNum": dbUnreadSeqNum,
|
||||
"err": err,
|
||||
}).Debug("DB counts")
|
||||
if err == nil {
|
||||
status.Messages = uint32(dbTotal)
|
||||
status.Unseen = uint32(dbUnread)
|
||||
status.UnseenSeqNum = uint32(dbUnreadSeqNum)
|
||||
}
|
||||
|
||||
if status.UidNext, err = im.storeMailbox.GetNextUID(); err != nil {
|
||||
@ -139,29 +145,19 @@ func (im *imapMailbox) Status(items []string) (*imap.MailboxStatus, error) {
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// Subscribe adds the mailbox to the server's set of "active" or "subscribed" mailboxes.
|
||||
func (im *imapMailbox) Subscribe() error {
|
||||
// SetSubscribed adds or removes the mailbox to the server's set of "active"
|
||||
// or "subscribed" mailboxes.
|
||||
func (im *imapMailbox) SetSubscribed(subscribed bool) error {
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
label := im.storeMailbox.LabelID()
|
||||
if !im.user.isSubscribed(label) {
|
||||
if subscribed && !im.user.isSubscribed(label) {
|
||||
im.user.removeFromCache(SubscriptionException, label)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unsubscribe removes the mailbox to the server's set of "active" or "subscribed" mailboxes.
|
||||
func (im *imapMailbox) Unsubscribe() error {
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
label := im.storeMailbox.LabelID()
|
||||
if im.user.isSubscribed(label) {
|
||||
if !subscribed && im.user.isSubscribed(label) {
|
||||
im.user.addToCache(SubscriptionException, label)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -29,9 +29,10 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/cache"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
@ -81,7 +82,11 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
|
||||
return errors.New("no available address for encryption")
|
||||
}
|
||||
m.AddressID = addr.ID
|
||||
kr := addr.KeyRing()
|
||||
|
||||
kr, err := im.user.client().KeyRingForAddressID(addr.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle imported messages which have no "Sender" address.
|
||||
// This sometimes occurs with outlook which reports errors as imported emails or for drafts.
|
||||
@ -126,10 +131,6 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
|
||||
|
||||
// We didn't find the message in the store, so we are currently sending it.
|
||||
logEntry.WithField("time", date).Info("No matching UID, continuing APPEND to Sent")
|
||||
|
||||
// For now we don't import user's own messages to Sent because GetUIDByHeader is not smart enough.
|
||||
// This will be fixed in GODT-143.
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is an APPEND to the Sent folder, so we will set the sent flag
|
||||
@ -148,10 +149,10 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
|
||||
if len(referenceList) > 0 {
|
||||
lastReference := referenceList[len(referenceList)-1]
|
||||
// In case we are using a mail client which corrupts headers, try "References" too.
|
||||
re := regexp.MustCompile("<[a-zA-Z0-9-_=]*@protonmail.internalid>")
|
||||
match := re.FindString(lastReference)
|
||||
if match != "" {
|
||||
internalID = match[1 : len(match)-len("@protonmail.internalid>")]
|
||||
re := regexp.MustCompile(pmapi.InternalReferenceFormat)
|
||||
match := re.FindStringSubmatch(lastReference)
|
||||
if len(match) > 0 {
|
||||
internalID = match[0]
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,7 +184,7 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
|
||||
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
|
||||
}
|
||||
|
||||
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *pmcrypto.KeyRing) (err error) { // nolint[funlen]
|
||||
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) { // nolint[funlen]
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
// Overwrite content for main header for import.
|
||||
@ -239,7 +240,7 @@ func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *
|
||||
}
|
||||
|
||||
// Create encrypted writer.
|
||||
pgpMessage, err := kr.Encrypt(pmcrypto.NewPlainMessage(data), nil)
|
||||
pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -265,7 +266,7 @@ func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *
|
||||
return im.storeMailbox.ImportMessage(m, b.Bytes(), labels)
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []string) (msg *imap.Message, err error) {
|
||||
func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem) (msg *imap.Message, err error) {
|
||||
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message")
|
||||
|
||||
seqNum, err := storeMessage.SequenceNumber()
|
||||
@ -278,9 +279,9 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []str
|
||||
msg = imap.NewMessage(seqNum, items)
|
||||
for _, item := range items {
|
||||
switch item {
|
||||
case imap.EnvelopeMsgAttr:
|
||||
case imap.FetchEnvelope:
|
||||
msg.Envelope = message.GetEnvelope(m)
|
||||
case imap.BodyMsgAttr, imap.BodyStructureMsgAttr:
|
||||
case imap.FetchBody, imap.FetchBodyStructure:
|
||||
var structure *message.BodyStructure
|
||||
if structure, _, err = im.getBodyStructure(storeMessage); err != nil {
|
||||
return
|
||||
@ -288,11 +289,11 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []str
|
||||
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
|
||||
return
|
||||
}
|
||||
case imap.FlagsMsgAttr:
|
||||
case imap.FetchFlags:
|
||||
msg.Flags = message.GetFlags(m)
|
||||
case imap.InternalDateMsgAttr:
|
||||
case imap.FetchInternalDate:
|
||||
msg.InternalDate = time.Unix(m.Time, 0)
|
||||
case imap.SizeMsgAttr:
|
||||
case imap.FetchRFC822Size:
|
||||
// Size attribute on the server counts encrypted data. The value is cleared
|
||||
// on our part and we need to compute "real" size of decrypted data.
|
||||
if m.Size <= 0 {
|
||||
@ -301,7 +302,7 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []str
|
||||
}
|
||||
}
|
||||
msg.Size = uint32(m.Size)
|
||||
case imap.UidMsgAttr:
|
||||
case imap.FetchUid:
|
||||
msg.Uid, err = storeMessage.UID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -310,7 +311,7 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []str
|
||||
s := item
|
||||
|
||||
var section *imap.BodySectionName
|
||||
if section, err = imap.NewBodySectionName(s); err != nil {
|
||||
if section, err = imap.ParseBodySectionName(s); err != nil {
|
||||
err = nil // Ignore error
|
||||
break
|
||||
}
|
||||
@ -421,13 +422,13 @@ func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider,
|
||||
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header.
|
||||
// Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
|
||||
response, err = structure.GetSectionContent(bodyReader, section.Path)
|
||||
case section.Specifier == imap.MimeSpecifier:
|
||||
case section.Specifier == imap.MIMESpecifier:
|
||||
// The MIME part specifier refers to the [MIME-IMB] header for this part.
|
||||
fallthrough
|
||||
case section.Specifier == imap.HeaderSpecifier:
|
||||
header, err = structure.GetSectionHeader(section.Path)
|
||||
default:
|
||||
err = errors.New("Unknown specifier " + section.Specifier)
|
||||
err = errors.New("Unknown specifier " + string(section.Specifier))
|
||||
}
|
||||
}
|
||||
|
||||
@ -488,28 +489,53 @@ func (im *imapMailbox) fetchMessage(m *pmapi.Message) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (im *imapMailbox) customMessage(m *pmapi.Message, err error, attachBody bool) {
|
||||
// Assuming quoted-printable.
|
||||
origBody := strings.Replace(m.Body, "=", "=3D", -1)
|
||||
m.Body = "Content-Type: text/html\r\n"
|
||||
m.Body = "\n<html><head></head><body style=3D\"font-family: Arial,'Helvetica Neue',Helvetica,sans-serif; font-size: 14px; \">\n"
|
||||
m.Body += "<div style=3D\"color:#555; background-color:#cf9696; padding:20px; border-radius: 4px;\" >\n<strong>Decryption error</strong><br/>Decryption of this message's encrypted content failed.<pre>\n"
|
||||
m.Body += err.Error()
|
||||
m.Body += "\n</pre></div>\n"
|
||||
const customMessageTemplate = `
|
||||
<html>
|
||||
<head></head>
|
||||
<body style="font-family: Arial,'Helvetica Neue',Helvetica,sans-serif; font-size: 14px;">
|
||||
<div style="color:#555; background-color:#cf9696; padding:20px; border-radius: 4px;">
|
||||
<strong>Decryption error</strong><br/>
|
||||
Decryption of this message's encrypted content failed.
|
||||
<pre>{{.Error}}</pre>
|
||||
</div>
|
||||
|
||||
if attachBody {
|
||||
m.Body += "<div style=3D\"color:#333; background-color:#f4f4f4; border: 1px solid #acb0bf; border-radius: 2px; padding:1rem; margin:1rem 0; font-family:monospace; font-size: 1em;\" ><pre>\n"
|
||||
m.Body += origBody
|
||||
m.Body += "\n</pre></div>\n"
|
||||
{{if .AttachBody}}
|
||||
<div style="color:#333; background-color:#f4f4f4; border: 1px solid #acb0bf; border-radius: 2px; padding:1rem; margin:1rem 0; font-family:monospace; font-size: 1em;">
|
||||
<pre>{{.Body}}</pre>
|
||||
</div>
|
||||
{{- end}}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
type customMessageData struct {
|
||||
Error string
|
||||
AttachBody bool
|
||||
Body string
|
||||
}
|
||||
|
||||
func (im *imapMailbox) makeCustomMessage(m *pmapi.Message, decodeError error, attachBody bool) (err error) {
|
||||
t := template.Must(template.New("customMessage").Parse(customMessageTemplate))
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
if err = t.Execute(b, customMessageData{
|
||||
Error: decodeError.Error(),
|
||||
AttachBody: attachBody,
|
||||
Body: m.Body,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.Body += "</body></html>"
|
||||
m.MIMEType = "text/html"
|
||||
m.MIMEType = pmapi.ContentTypeHTML
|
||||
m.Body = b.String()
|
||||
|
||||
// NOTE: we need to set header in custom message header, so we check that is non-nil.
|
||||
if m.Header == nil {
|
||||
m.Header = make(mail.Header)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err error) {
|
||||
@ -522,10 +548,16 @@ func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err erro
|
||||
}
|
||||
}
|
||||
|
||||
kr := im.user.client.KeyRingForAddressID(m.AddressID)
|
||||
kr, err := im.user.client().KeyRingForAddressID(m.AddressID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get keyring for address ID")
|
||||
}
|
||||
|
||||
err = message.WriteBody(w, kr, m)
|
||||
if err != nil {
|
||||
im.customMessage(m, err, true)
|
||||
if customMessageErr := im.makeCustomMessage(m, err, true); customMessageErr != nil {
|
||||
im.log.WithError(customMessageErr).Warn("Failed to make custom message")
|
||||
}
|
||||
_, _ = io.WriteString(w, m.Body)
|
||||
err = nil
|
||||
}
|
||||
@ -546,13 +578,17 @@ func (im *imapMailbox) writeAndParseMIMEBody(m *pmapi.Message) (mime *enmime.Env
|
||||
|
||||
func (im *imapMailbox) writeAttachmentBody(w io.Writer, m *pmapi.Message, att *pmapi.Attachment) (err error) {
|
||||
// Retrieve encrypted attachment.
|
||||
r, err := im.user.client.GetAttachment(att.ID)
|
||||
r, err := im.user.client().GetAttachment(att.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer r.Close() //nolint[errcheck]
|
||||
|
||||
kr := im.user.client.KeyRingForAddressID(m.AddressID)
|
||||
kr, err := im.user.client().KeyRingForAddressID(m.AddressID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get keyring for address ID")
|
||||
}
|
||||
|
||||
if err = message.WriteAttachmentBody(w, kr, m, att, r); err != nil {
|
||||
// Returning an error here makes certain mail clients behave badly,
|
||||
// trying to retrieve the message again and again.
|
||||
@ -646,12 +682,19 @@ func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodySt
|
||||
return
|
||||
}
|
||||
|
||||
kr := im.user.client.KeyRingForAddressID(m.AddressID)
|
||||
kr, err := im.user.client().KeyRingForAddressID(m.AddressID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "failed to get keyring for address ID")
|
||||
return
|
||||
}
|
||||
|
||||
errDecrypt := m.Decrypt(kr)
|
||||
|
||||
if errDecrypt != nil && errDecrypt != openpgperrors.ErrSignatureExpired {
|
||||
errNoCache.add(errDecrypt)
|
||||
im.customMessage(m, errDecrypt, true)
|
||||
if customMessageErr := im.makeCustomMessage(m, errDecrypt, true); customMessageErr != nil {
|
||||
im.log.WithError(customMessageErr).Warn("Failed to make custom message")
|
||||
}
|
||||
}
|
||||
|
||||
// Inner function can fail even when message is decrypted.
|
||||
@ -665,7 +708,9 @@ func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodySt
|
||||
return nil, nil, err
|
||||
} else if err != nil {
|
||||
errNoCache.add(err)
|
||||
im.customMessage(m, err, true)
|
||||
if customMessageErr := im.makeCustomMessage(m, err, true); customMessageErr != nil {
|
||||
im.log.WithError(customMessageErr).Warn("Failed to make custom message")
|
||||
}
|
||||
structure, msgBody, err = im.buildMessageInner(m, kr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -677,7 +722,7 @@ func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodySt
|
||||
return structure, msgBody, err
|
||||
}
|
||||
|
||||
func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *pmcrypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen]
|
||||
func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen]
|
||||
multipartType, err := im.setMessageContentType(m)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@ -102,6 +102,21 @@ func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel s
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
return im.labelMessages(uid, seqSet, targetLabel, false)
|
||||
}
|
||||
|
||||
// MoveMessages adds dest's label and removes this mailbox' label from each message.
|
||||
//
|
||||
// This should not be used until MOVE extension has option to send UIDPLUS
|
||||
// responses.
|
||||
func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
return im.labelMessages(uid, seqSet, targetLabel, true)
|
||||
}
|
||||
|
||||
func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel string, move bool) error {
|
||||
messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
|
||||
if err != nil || len(messageIDs) == 0 {
|
||||
return err
|
||||
@ -111,40 +126,24 @@ func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel s
|
||||
// messages can be removed from source during labeling (e.g. folder1 -> folder2).
|
||||
sourceSeqSet := im.storeMailbox.GetUIDList(messageIDs)
|
||||
|
||||
targetStoreMBX, err := im.storeAddress.GetMailbox(targetLabel)
|
||||
targetStoreMailbox, err := im.storeAddress.GetMailbox(targetLabel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = targetStoreMBX.LabelMessages(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetSeqSet := targetStoreMBX.GetUIDList(messageIDs)
|
||||
return uidplus.CopyResponse(im.storeMailbox.UIDValidity(), sourceSeqSet, targetSeqSet)
|
||||
}
|
||||
|
||||
// MoveMessages adds dest's label and removes this mailbox' label from each message.
|
||||
//
|
||||
// This should not be used until MOVE extension has option to send UIDPLUS
|
||||
// responses.
|
||||
func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, newLabel string) error {
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
|
||||
if err != nil || len(messageIDs) == 0 {
|
||||
return err
|
||||
}
|
||||
storeMailbox, err := im.storeAddress.GetMailbox(newLabel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Label messages first to not loss them. If message is only in trash and we unlabel
|
||||
// Label messages first to not lose them. If message is only in trash and we unlabel
|
||||
// it, it will be removed completely and we cannot label it back.
|
||||
if err := storeMailbox.LabelMessages(messageIDs); err != nil {
|
||||
if err := targetStoreMailbox.LabelMessages(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
return im.storeMailbox.UnlabelMessages(messageIDs)
|
||||
if move {
|
||||
if err := im.storeMailbox.UnlabelMessages(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
targetSeqSet := targetStoreMailbox.GetUIDList(messageIDs)
|
||||
return uidplus.CopyResponse(targetStoreMailbox.UIDValidity(), sourceSeqSet, targetSeqSet)
|
||||
}
|
||||
|
||||
// SearchMessages searches messages. The returned list must contain UIDs if
|
||||
@ -153,17 +152,17 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
if criteria.Not != nil || criteria.Or[0] != nil {
|
||||
if criteria.Not != nil || criteria.Or != nil {
|
||||
return nil, errors.New("unsupported search query")
|
||||
}
|
||||
|
||||
if criteria.Body != "" || criteria.Text != "" {
|
||||
if criteria.Body != nil || criteria.Text != nil {
|
||||
log.Warn("Body and Text criteria not applied.")
|
||||
}
|
||||
|
||||
var apiIDs []string
|
||||
if criteria.SeqSet != nil {
|
||||
apiIDs, err = im.apiIDsFromSeqSet(false, criteria.SeqSet)
|
||||
if criteria.SeqNum != nil {
|
||||
apiIDs, err = im.apiIDsFromSeqSet(false, criteria.SeqNum)
|
||||
} else {
|
||||
apiIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(1, 0)
|
||||
}
|
||||
@ -171,20 +170,15 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiIDsFromUID []string
|
||||
if criteria.Uid != nil {
|
||||
if apiIDs, err := im.apiIDsFromSeqSet(true, criteria.Uid); err == nil {
|
||||
apiIDsFromUID = append(apiIDsFromUID, apiIDs...)
|
||||
apiIDsByUID, err := im.apiIDsFromSeqSet(true, criteria.Uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiIDs = arrayIntersection(apiIDs, apiIDsByUID)
|
||||
}
|
||||
|
||||
// Apply filters.
|
||||
for _, apiID := range apiIDs {
|
||||
// Filter on UIDs.
|
||||
if len(apiIDsFromUID) > 0 && !isStringInList(apiIDsFromUID, apiID) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get message.
|
||||
storeMessage, err := im.storeMailbox.GetMessage(apiID)
|
||||
if err != nil {
|
||||
@ -193,79 +187,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
}
|
||||
m := storeMessage.Message()
|
||||
|
||||
// Filter addresses.
|
||||
if criteria.From != "" && !addressMatch([]*mail.Address{m.Sender}, criteria.From) {
|
||||
continue
|
||||
}
|
||||
if criteria.To != "" && !addressMatch(m.ToList, criteria.To) {
|
||||
continue
|
||||
}
|
||||
if criteria.Cc != "" && !addressMatch(m.CCList, criteria.Cc) {
|
||||
continue
|
||||
}
|
||||
if criteria.Bcc != "" && !addressMatch(m.BCCList, criteria.Bcc) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter strings.
|
||||
if criteria.Subject != "" && !strings.Contains(strings.ToLower(m.Subject), strings.ToLower(criteria.Subject)) {
|
||||
continue
|
||||
}
|
||||
if criteria.Keyword != "" && !hasKeyword(m, criteria.Keyword) {
|
||||
continue
|
||||
}
|
||||
if criteria.Unkeyword != "" && hasKeyword(m, criteria.Unkeyword) {
|
||||
continue
|
||||
}
|
||||
if criteria.Header[0] != "" {
|
||||
h := message.GetHeader(m)
|
||||
if val := h.Get(criteria.Header[0]); val == "" {
|
||||
continue // Field is not in header.
|
||||
} else if criteria.Header[1] != "" && !strings.Contains(strings.ToLower(val), strings.ToLower(criteria.Header[1])) {
|
||||
continue // Field is in header, second criteria is non-zero and field value not matched (case insensitive).
|
||||
}
|
||||
}
|
||||
|
||||
// Filter flags.
|
||||
if criteria.Flagged && !isStringInList(m.LabelIDs, pmapi.StarredLabel) {
|
||||
continue
|
||||
}
|
||||
if criteria.Unflagged && isStringInList(m.LabelIDs, pmapi.StarredLabel) {
|
||||
continue
|
||||
}
|
||||
if criteria.Seen && m.Unread == 1 {
|
||||
continue
|
||||
}
|
||||
if criteria.Unseen && m.Unread == 0 {
|
||||
continue
|
||||
}
|
||||
if criteria.Deleted {
|
||||
continue
|
||||
}
|
||||
// if criteria.Undeleted { // All messages matches this criteria }
|
||||
if criteria.Draft && (m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived)) {
|
||||
continue
|
||||
}
|
||||
if criteria.Undraft && !(m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived)) {
|
||||
continue
|
||||
}
|
||||
if criteria.Answered && !(m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll)) {
|
||||
continue
|
||||
}
|
||||
if criteria.Unanswered && (m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll)) {
|
||||
continue
|
||||
}
|
||||
if criteria.Recent && m.Has(pmapi.FlagOpened) { // opened means not recent
|
||||
continue
|
||||
}
|
||||
if criteria.Old && !m.Has(pmapi.FlagOpened) {
|
||||
continue
|
||||
}
|
||||
if criteria.New && !(!m.Has(pmapi.FlagOpened) && m.Unread == 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter internal date.
|
||||
// Filter by time.
|
||||
if !criteria.Before.IsZero() {
|
||||
if truncated := criteria.Before.Truncate(24 * time.Hour); m.Time > truncated.Unix() {
|
||||
continue
|
||||
@ -276,15 +198,8 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !criteria.On.IsZero() {
|
||||
truncated := criteria.On.Truncate(24 * time.Hour)
|
||||
if m.Time < truncated.Unix() || m.Time > truncated.Add(24*time.Hour).Unix() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !(criteria.SentBefore.IsZero() && criteria.SentSince.IsZero() && criteria.SentOn.IsZero()) {
|
||||
if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() {
|
||||
if t, err := m.Header.Date(); err == nil && !t.IsZero() {
|
||||
// Filter header date.
|
||||
if !criteria.SentBefore.IsZero() {
|
||||
if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() {
|
||||
continue
|
||||
@ -295,16 +210,81 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !criteria.SentOn.IsZero() {
|
||||
truncated := criteria.SentOn.Truncate(24 * time.Hour)
|
||||
if t.Unix() < truncated.Unix() || t.Unix() > truncated.Add(24*time.Hour).Unix() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter size (only if size was already calculated).
|
||||
// Filter by headers.
|
||||
header := message.GetHeader(m)
|
||||
headerMatch := true
|
||||
for criteriaKey, criteriaValues := range criteria.Header {
|
||||
for _, criteriaValue := range criteriaValues {
|
||||
if criteriaValue == "" {
|
||||
continue
|
||||
}
|
||||
switch criteriaKey {
|
||||
case "From":
|
||||
headerMatch = addressMatch([]*mail.Address{m.Sender}, criteriaValue)
|
||||
case "To":
|
||||
headerMatch = addressMatch(m.ToList, criteriaValue)
|
||||
case "Cc":
|
||||
headerMatch = addressMatch(m.CCList, criteriaValue)
|
||||
case "Bcc":
|
||||
headerMatch = addressMatch(m.BCCList, criteriaValue)
|
||||
default:
|
||||
if messageValue := header.Get(criteriaKey); messageValue == "" {
|
||||
headerMatch = false // Field is not in header.
|
||||
} else if !strings.Contains(strings.ToLower(messageValue), strings.ToLower(criteriaValue)) {
|
||||
headerMatch = false // Field is in header but value not matched (case insensitive).
|
||||
}
|
||||
}
|
||||
if !headerMatch {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !headerMatch {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !headerMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by flags.
|
||||
messageFlagsMap := make(map[string]bool)
|
||||
if isStringInList(m.LabelIDs, pmapi.StarredLabel) {
|
||||
messageFlagsMap[imap.FlaggedFlag] = true
|
||||
}
|
||||
if m.Unread == 0 {
|
||||
messageFlagsMap[imap.SeenFlag] = true
|
||||
}
|
||||
if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) {
|
||||
messageFlagsMap[imap.AnsweredFlag] = true
|
||||
}
|
||||
if m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived) {
|
||||
messageFlagsMap[imap.DraftFlag] = true
|
||||
}
|
||||
if !m.Has(pmapi.FlagOpened) {
|
||||
messageFlagsMap[imap.RecentFlag] = true
|
||||
}
|
||||
|
||||
flagMatch := true
|
||||
for _, flag := range criteria.WithFlags {
|
||||
if !messageFlagsMap[flag] {
|
||||
flagMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, flag := range criteria.WithoutFlags {
|
||||
if messageFlagsMap[flag] {
|
||||
flagMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !flagMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by size (only if size was already calculated).
|
||||
if m.Size > 0 {
|
||||
if criteria.Larger != 0 && m.Size <= int64(criteria.Larger) {
|
||||
continue
|
||||
@ -338,7 +318,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
// 3501 section 6.4.5 for a list of items that can be requested.
|
||||
//
|
||||
// Messages must be sent to msgResponse. When the function returns, msgResponse must be closed.
|
||||
func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []string, msgResponse chan<- *imap.Message) (err error) { //nolint[funlen]
|
||||
func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) (err error) { //nolint[funlen]
|
||||
defer func() {
|
||||
close(msgResponse)
|
||||
if err != nil {
|
||||
@ -453,13 +433,17 @@ func (im *imapMailbox) apiIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]string
|
||||
return apiIDs, nil
|
||||
}
|
||||
|
||||
func isAddressInList(addrs []*mail.Address, query string) bool { //nolint[deadcode]
|
||||
for _, addr := range addrs {
|
||||
if strings.Contains(addr.Address, query) || strings.Contains(addr.Name, query) {
|
||||
return true
|
||||
func arrayIntersection(a, b []string) (c []string) {
|
||||
m := make(map[string]bool)
|
||||
for _, item := range a {
|
||||
m[item] = true
|
||||
}
|
||||
for _, item := range b {
|
||||
if _, ok := m[item]; ok {
|
||||
c = append(c, item)
|
||||
}
|
||||
}
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
func isStringInList(list []string, s string) bool {
|
||||
@ -479,12 +463,3 @@ func addressMatch(addresses []*mail.Address, criteria string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasKeyword(m *pmapi.Message, keyword string) bool {
|
||||
for _, v := range message.GetHeader(m) {
|
||||
if strings.Contains(strings.ToLower(strings.Join(v, " ")), strings.ToLower(keyword)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -71,29 +71,25 @@ func (m *imapRootMailbox) Info() (info *imap.MailboxInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (m *imapRootMailbox) Status(items []string) (status *imap.MailboxStatus, err error) {
|
||||
status = &imap.MailboxStatus{}
|
||||
func (m *imapRootMailbox) Status(_ []imap.StatusItem) (*imap.MailboxStatus, error) {
|
||||
status := &imap.MailboxStatus{}
|
||||
if m.isFolder {
|
||||
status.Name = store.UserFoldersMailboxName
|
||||
} else {
|
||||
status.Name = store.UserLabelsMailboxName
|
||||
}
|
||||
return
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (m *imapRootMailbox) Subscribe() error {
|
||||
return errors.New("cannot subscribe to Labels or Folders mailboxes")
|
||||
}
|
||||
|
||||
func (m *imapRootMailbox) Unsubscribe() error {
|
||||
return errors.New("cannot unsubscribe from Labels or Folders mailboxes")
|
||||
func (m *imapRootMailbox) SetSubscribed(_ bool) error {
|
||||
return errors.New("cannot subscribe or unsubsribe to Labels or Folders mailboxes")
|
||||
}
|
||||
|
||||
func (m *imapRootMailbox) Check() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *imapRootMailbox) ListMessages(uid bool, seqset *imap.SeqSet, items []string, ch chan<- *imap.Message) error {
|
||||
func (m *imapRootMailbox) ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error {
|
||||
close(ch)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -32,6 +33,7 @@ import (
|
||||
"github.com/emersion/go-imap"
|
||||
imapappendlimit "github.com/emersion/go-imap-appendlimit"
|
||||
imapidle "github.com/emersion/go-imap-idle"
|
||||
imapmove "github.com/emersion/go-imap-move"
|
||||
imapquota "github.com/emersion/go-imap-quota"
|
||||
imapspecialuse "github.com/emersion/go-imap-specialuse"
|
||||
imapunselect "github.com/emersion/go-imap-unselect"
|
||||
@ -43,6 +45,8 @@ import (
|
||||
type imapServer struct {
|
||||
server *imapserver.Server
|
||||
eventListener listener.Listener
|
||||
debugClient bool
|
||||
debugServer bool
|
||||
}
|
||||
|
||||
// NewIMAPServer constructs a new IMAP server configured with the given options.
|
||||
@ -54,17 +58,6 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
s.ErrorLog = newServerErrorLogger("server-imap")
|
||||
s.AutoLogout = 30 * time.Minute
|
||||
|
||||
if debugClient || debugServer {
|
||||
var localDebug, remoteDebug imap.WriterWithFields
|
||||
if debugClient {
|
||||
remoteDebug = &logWithFields{log: log.WithField("pkg", "imap/client"), fields: logrus.Fields{}}
|
||||
}
|
||||
if debugServer {
|
||||
localDebug = &logWithFields{log: log.WithField("pkg", "imap/server"), fields: logrus.Fields{}}
|
||||
}
|
||||
s.Debug = imap.NewDebugWithFields(localDebug, remoteDebug)
|
||||
}
|
||||
|
||||
serverID := imapid.ID{
|
||||
imapid.FieldName: "ProtonMail",
|
||||
imapid.FieldVendor: "Proton Technologies AG",
|
||||
@ -75,14 +68,27 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
conn.Server().ForEachConn(func(candidate imapserver.Conn) {
|
||||
if id, ok := candidate.(imapid.Conn); ok {
|
||||
if conn.Context() == candidate.Context() {
|
||||
// ID is not available right at the beginning of the connection.
|
||||
// Clients send ID quickly after AUTH. We need to wait for it.
|
||||
go func() {
|
||||
start := time.Now()
|
||||
for {
|
||||
if id.ID() != nil {
|
||||
imapBackend.setLastMailClient(id.ID())
|
||||
return
|
||||
break
|
||||
}
|
||||
if time.Since(start) > 10*time.Second {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return sasl.NewLoginServer(func(address, password string) error {
|
||||
user, err := conn.Server().Backend.Login(address, password)
|
||||
user, err := conn.Server().Backend.Login(nil, address, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -96,7 +102,7 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
|
||||
s.Enable(
|
||||
imapidle.NewExtension(),
|
||||
//imapmove.NewExtension(), // extension is not fully implemented: if UIDPLUS exists it MUST return COPYUID and EXPUNGE continuous responses
|
||||
imapmove.NewExtension(),
|
||||
imapspecialuse.NewExtension(),
|
||||
imapid.NewExtension(serverID),
|
||||
imapquota.NewExtension(),
|
||||
@ -108,6 +114,8 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
return &imapServer{
|
||||
server: s,
|
||||
eventListener: eventListener,
|
||||
debugClient: debugClient,
|
||||
debugServer: debugServer,
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +124,17 @@ func (s *imapServer) ListenAndServe() {
|
||||
go s.monitorDisconnectedUsers()
|
||||
|
||||
log.Info("IMAP server listening at ", s.server.Addr)
|
||||
err := s.server.ListenAndServe()
|
||||
l, err := net.Listen("tcp", s.server.Addr)
|
||||
if err != nil {
|
||||
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
|
||||
log.Error("IMAP failed: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.server.Serve(&debugListener{
|
||||
Listener: l,
|
||||
server: s,
|
||||
})
|
||||
if err != nil {
|
||||
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
|
||||
log.Error("IMAP failed: ", err)
|
||||
@ -149,20 +167,38 @@ func (s *imapServer) monitorDisconnectedUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
// logWithFields is used for debuging with additional field.
|
||||
type logWithFields struct {
|
||||
log *logrus.Entry
|
||||
fields logrus.Fields
|
||||
// debugListener sets debug loggers on server containing fields with local
|
||||
// and remote addresses right after new connection is accepted.
|
||||
type debugListener struct {
|
||||
net.Listener
|
||||
|
||||
server *imapServer
|
||||
}
|
||||
|
||||
func (lf *logWithFields) Writer() io.Writer {
|
||||
w := lf.log.WithFields(lf.fields).WriterLevel(logrus.DebugLevel)
|
||||
lf.fields = logrus.Fields{}
|
||||
return w
|
||||
}
|
||||
func (dl *debugListener) Accept() (net.Conn, error) {
|
||||
conn, err := dl.Listener.Accept()
|
||||
|
||||
func (lf *logWithFields) SetField(key, value string) {
|
||||
lf.fields[key] = value
|
||||
if err == nil && (dl.server.debugServer || dl.server.debugClient) {
|
||||
debugLog := log
|
||||
if addr := conn.LocalAddr(); addr != nil {
|
||||
debugLog = debugLog.WithField("loc", addr.String())
|
||||
}
|
||||
if addr := conn.RemoteAddr(); addr != nil {
|
||||
debugLog = debugLog.WithField("rem", addr.String())
|
||||
}
|
||||
|
||||
var localDebug, remoteDebug io.Writer
|
||||
if dl.server.debugServer {
|
||||
localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel)
|
||||
}
|
||||
if dl.server.debugClient {
|
||||
remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
dl.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
|
||||
}
|
||||
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// serverErrorLogger implements go-imap/logger interface.
|
||||
@ -174,17 +210,12 @@ func newServerErrorLogger(tag string) *serverErrorLogger {
|
||||
return &serverErrorLogger{tag}
|
||||
}
|
||||
|
||||
func (s *serverErrorLogger) CheckErrorForReport(serverErr string) {
|
||||
}
|
||||
|
||||
func (s *serverErrorLogger) Printf(format string, args ...interface{}) {
|
||||
err := fmt.Sprintf(format, args...)
|
||||
s.CheckErrorForReport(err)
|
||||
log.WithField("pkg", s.tag).Error(err)
|
||||
}
|
||||
|
||||
func (s *serverErrorLogger) Println(args ...interface{}) {
|
||||
err := fmt.Sprintln(args...)
|
||||
s.CheckErrorForReport(err)
|
||||
log.WithField("pkg", s.tag).Error(err)
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"io"
|
||||
"net/mail"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -35,7 +35,7 @@ type storeUserProvider interface {
|
||||
GetAddress(addressID string) (storeAddressProvider, error)
|
||||
|
||||
CreateDraft(
|
||||
kr *pmcrypto.KeyRing,
|
||||
kr *crypto.KeyRing,
|
||||
message *pmapi.Message,
|
||||
attachmentReaders []io.Reader,
|
||||
attachedPublicKey,
|
||||
@ -68,7 +68,7 @@ type storeMailboxProvider interface {
|
||||
GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error)
|
||||
GetLatestAPIID() (string, error)
|
||||
GetNextUID() (uint32, error)
|
||||
GetCounts() (dbTotal, dbUnread uint, err error)
|
||||
GetCounts() (dbTotal, dbUnread, dbUnreadSeqNum uint, err error)
|
||||
GetUIDList(apiIDs []string) *uidplus.OrderedSeq
|
||||
GetUIDByHeader(header *mail.Header) uint32
|
||||
GetDelimiter() string
|
||||
|
||||
@ -38,11 +38,11 @@ const Capability = "UIDPLUS"
|
||||
const (
|
||||
copyuid = "COPYUID"
|
||||
appenduid = "APPENDUID"
|
||||
copySuccess = "COPY successful"
|
||||
appendSucess = "APPEND successful"
|
||||
copySuccess = "COPY completed"
|
||||
appendSucess = "APPEND completed"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "impa/uidplus") //nolint[gochecknoglobals]
|
||||
var log = logrus.WithField("pkg", "imap/uidplus") //nolint[gochecknoglobals]
|
||||
|
||||
// OrderedSeq to remember Seq in order they are added.
|
||||
// We didn't find any restriction in RFC that server must respond with ranges
|
||||
@ -141,7 +141,7 @@ func (ext *extension) Capabilities(c server.Conn) []string {
|
||||
}
|
||||
|
||||
func (ext *extension) Command(name string) server.HandlerFactory {
|
||||
if name == imap.Expunge {
|
||||
if name == "EXPUNGE" {
|
||||
return func() server.Handler {
|
||||
return &UIDExpunge{}
|
||||
}
|
||||
@ -165,7 +165,7 @@ func getStatusResponseCopy(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq)
|
||||
}
|
||||
|
||||
return &imap.StatusResp{
|
||||
Type: imap.StatusOk,
|
||||
Type: imap.StatusRespOk,
|
||||
Info: info,
|
||||
}
|
||||
}
|
||||
@ -187,7 +187,7 @@ func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.St
|
||||
}
|
||||
|
||||
return &imap.StatusResp{
|
||||
Type: imap.StatusOk,
|
||||
Type: imap.StatusRespOk,
|
||||
Info: info,
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
imapquota "github.com/emersion/go-imap-quota"
|
||||
goIMAPBackend "github.com/emersion/go-imap/backend"
|
||||
)
|
||||
@ -34,7 +34,6 @@ type imapUser struct {
|
||||
panicHandler panicHandler
|
||||
backend *imapBackend
|
||||
user bridgeUser
|
||||
client bridge.PMAPIProvider
|
||||
|
||||
storeUser storeUserProvider
|
||||
storeAddress storeAddressProvider
|
||||
@ -42,6 +41,11 @@ type imapUser struct {
|
||||
currentAddressLowercase string
|
||||
}
|
||||
|
||||
// This method should eventually no longer be necessary. Everything should go via store.
|
||||
func (iu *imapUser) client() pmapi.Client {
|
||||
return iu.user.GetTemporaryPMAPIClient()
|
||||
}
|
||||
|
||||
// newIMAPUser returns struct implementing go-imap/user interface.
|
||||
func newIMAPUser(
|
||||
panicHandler panicHandler,
|
||||
@ -62,13 +66,10 @@ func newIMAPUser(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := user.GetTemporaryPMAPIClient()
|
||||
|
||||
return &imapUser{
|
||||
panicHandler: panicHandler,
|
||||
backend: backend,
|
||||
user: user,
|
||||
client: client,
|
||||
|
||||
storeUser: storeUser,
|
||||
storeAddress: storeAddress,
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build pmapi_prod
|
||||
|
||||
// Package pmapifactory creates pmapi client instances.
|
||||
package pmapifactory
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func New(config bridge.Configer, listener listener.Listener) bridge.PMAPIProviderFactory {
|
||||
cfg := config.GetAPIConfig()
|
||||
|
||||
pin := pmapi.NewPMAPIPinning(cfg.AppVersion)
|
||||
pin.ReportCertIssueLocal = func() {
|
||||
listener.Emit(events.TLSCertIssue, "")
|
||||
}
|
||||
|
||||
// This transport already has timeouts set governing the roundtrip:
|
||||
// - IdleConnTimeout: 5 * time.Minute,
|
||||
// - ExpectContinueTimeout: 500 * time.Millisecond,
|
||||
// - ResponseHeaderTimeout: 30 * time.Second,
|
||||
cfg.Transport = pin.TransportWithPinning()
|
||||
|
||||
// We set additional timeouts/thresholds for the request as a whole:
|
||||
cfg.Timeout = 10 * time.Minute // Overall request timeout (~25MB / 10 mins => ~40kB/s, should be reasonable).
|
||||
cfg.FirstReadTimeout = 30 * time.Second // 30s to match 30s response header timeout.
|
||||
cfg.MinSpeed = 1 << 13 // Enforce minimum download speed of 8kB/s.
|
||||
|
||||
return func(userID string) bridge.PMAPIProvider {
|
||||
return pmapi.NewClient(cfg, userID)
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,8 @@ package smtp
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type bridger interface {
|
||||
@ -29,7 +31,7 @@ type bridgeUser interface {
|
||||
CheckBridgeLogin(password string) error
|
||||
IsCombinedAddressMode() bool
|
||||
GetAddressID(address string) (string, error)
|
||||
GetTemporaryPMAPIClient() bridge.PMAPIProvider
|
||||
GetTemporaryPMAPIClient() pmapi.Client
|
||||
GetStore() storeUserProvider
|
||||
}
|
||||
|
||||
@ -53,10 +55,10 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
|
||||
}
|
||||
|
||||
type bridgeUserWrap struct {
|
||||
*bridge.User
|
||||
*users.User
|
||||
}
|
||||
|
||||
func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap {
|
||||
func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
|
||||
return &bridgeUserWrap{User: bridgeUser}
|
||||
}
|
||||
|
||||
|
||||
75
internal/smtp/repro_test.go
Normal file
75
internal/smtp/repro_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
// 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 smtp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKeyRingsAreEqualAfterFiltering(t *testing.T) {
|
||||
// Load the key.
|
||||
key, err := crypto.NewKeyFromArmored(testPublicKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Put it in a keyring.
|
||||
keyRing, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Filter out expired ones.
|
||||
validKeyRings, err := crypto.FilterExpiredKeys([]*crypto.KeyRing{keyRing})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Filtering shouldn't make them unequal.
|
||||
assert.True(t, isEqual(t, keyRing, validKeyRings[0]))
|
||||
}
|
||||
|
||||
func isEqual(t *testing.T, a, b *crypto.KeyRing) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if a == nil && b != nil || a != nil && b == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
aKeys, bKeys := a.GetKeys(), b.GetKeys()
|
||||
|
||||
if len(aKeys) != len(bKeys) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range aKeys {
|
||||
aFPs := aKeys[i].GetSHA256Fingerprints()
|
||||
bFPs := bKeys[i].GetSHA256Fingerprints()
|
||||
|
||||
if !assert.Equal(t, aFPs, bFPs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -20,7 +20,7 @@ package smtp
|
||||
import (
|
||||
"errors"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/algo"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
@ -37,7 +37,7 @@ type SendingInfo struct {
|
||||
Sign bool
|
||||
Scheme int
|
||||
MIMEType string
|
||||
PublicKey *pmcrypto.KeyRing
|
||||
PublicKey *crypto.KeyRing
|
||||
}
|
||||
|
||||
func generateSendingInfo(
|
||||
@ -46,10 +46,10 @@ func generateSendingInfo(
|
||||
isInternal bool,
|
||||
composeMode string,
|
||||
apiKeys,
|
||||
contactKeys []*pmcrypto.KeyRing,
|
||||
contactKeys []*crypto.KeyRing,
|
||||
settingsSign bool,
|
||||
settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
|
||||
contactKeys, err = pmcrypto.FilterExpiredKeys(contactKeys)
|
||||
contactKeys, err = crypto.FilterExpiredKeys(contactKeys)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -72,7 +72,7 @@ func generateInternalSendingInfo(
|
||||
contactMeta *ContactMetadata,
|
||||
composeMode string,
|
||||
apiKeys,
|
||||
contactKeys []*pmcrypto.KeyRing,
|
||||
contactKeys []*crypto.KeyRing,
|
||||
settingsSign bool, //nolint[unparam]
|
||||
settingsPgpScheme int) (sendingInfo SendingInfo, err error) { //nolint[unparam]
|
||||
// If sending internally, there should always be a public key; if not, there's an error.
|
||||
@ -125,7 +125,7 @@ func generateExternalSendingInfo(
|
||||
contactMeta *ContactMetadata,
|
||||
composeMode string,
|
||||
apiKeys,
|
||||
contactKeys []*pmcrypto.KeyRing,
|
||||
contactKeys []*crypto.KeyRing,
|
||||
settingsSign bool,
|
||||
settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
|
||||
// The default settings, unless overridden by presence of a saved contact.
|
||||
@ -230,14 +230,27 @@ func schemeAndMIME(contact *ContactMetadata, settingsScheme int, settingsMIMETyp
|
||||
|
||||
// checkContactKeysAgainstAPI keeps only those contact keys which are up to date and have
|
||||
// an ID that matches an API key's ID.
|
||||
func checkContactKeysAgainstAPI(contactKeys, apiKeys []*pmcrypto.KeyRing) (filteredKeys []*pmcrypto.KeyRing, err error) { //nolint[unparam]
|
||||
func checkContactKeysAgainstAPI(contactKeys, apiKeys []*crypto.KeyRing) (filteredKeys []*crypto.KeyRing, err error) { //nolint[unparam]
|
||||
keyIDsAreEqual := func(a, b interface{}) bool {
|
||||
aKey, bKey := a.(*pmcrypto.KeyRing), b.(*pmcrypto.KeyRing)
|
||||
return aKey.GetEntities()[0].PrimaryKey.KeyId == bKey.GetEntities()[0].PrimaryKey.KeyId
|
||||
aKey, bKey := a.(*crypto.KeyRing), b.(*crypto.KeyRing)
|
||||
|
||||
aFirst, getKeyErr := aKey.GetKey(0)
|
||||
if getKeyErr != nil {
|
||||
err = errors.New("missing primary key")
|
||||
return false
|
||||
}
|
||||
|
||||
bFirst, getKeyErr := bKey.GetKey(0)
|
||||
if getKeyErr != nil {
|
||||
err = errors.New("missing primary key")
|
||||
return false
|
||||
}
|
||||
|
||||
return aFirst.GetKeyID() == bFirst.GetKeyID()
|
||||
}
|
||||
|
||||
for _, v := range algo.SetIntersection(contactKeys, apiKeys, keyIDsAreEqual) {
|
||||
filteredKeys = append(filteredKeys, v.(*pmcrypto.KeyRing))
|
||||
filteredKeys = append(filteredKeys, v.(*crypto.KeyRing))
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@ -18,36 +18,36 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mocks struct {
|
||||
t *testing.T
|
||||
eventListener *bridge.MockListener
|
||||
eventListener *users.MockListener
|
||||
}
|
||||
|
||||
func initMocks(t *testing.T) mocks {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
return mocks{
|
||||
t: t,
|
||||
eventListener: bridge.NewMockListener(mockCtrl),
|
||||
eventListener: users.NewMockListener(mockCtrl),
|
||||
}
|
||||
}
|
||||
|
||||
type args struct {
|
||||
eventListener listener.Listener
|
||||
contactMeta *ContactMetadata
|
||||
apiKeys []*pmcrypto.KeyRing
|
||||
contactKeys []*pmcrypto.KeyRing
|
||||
apiKeys []*crypto.KeyRing
|
||||
contactKeys []*crypto.KeyRing
|
||||
composeMode string
|
||||
settingsPgpScheme int
|
||||
settingsSign bool
|
||||
@ -68,18 +68,61 @@ func (tt *testData) runTest(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, gotSendingInfo, tt.wantSendingInfo)
|
||||
|
||||
assert.Equal(t, gotSendingInfo.Encrypt, tt.wantSendingInfo.Encrypt)
|
||||
assert.Equal(t, gotSendingInfo.Sign, tt.wantSendingInfo.Sign)
|
||||
assert.Equal(t, gotSendingInfo.Scheme, tt.wantSendingInfo.Scheme)
|
||||
assert.Equal(t, gotSendingInfo.MIMEType, tt.wantSendingInfo.MIMEType)
|
||||
assert.True(t, keyRingsAreEqual(gotSendingInfo.PublicKey, tt.wantSendingInfo.PublicKey))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func keyRingFromKey(publicKey string) *crypto.KeyRing {
|
||||
key, err := crypto.NewKeyFromArmored(publicKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return kr
|
||||
}
|
||||
|
||||
func keyRingsAreEqual(a, b *crypto.KeyRing) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if a == nil && b != nil || a != nil && b == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
aKeys, bKeys := a.GetKeys(), b.GetKeys()
|
||||
|
||||
if len(aKeys) != len(bKeys) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range aKeys {
|
||||
aFPs := aKeys[i].GetSHA256Fingerprints()
|
||||
bFPs := bKeys[i].GetSHA256Fingerprints()
|
||||
|
||||
if !cmp.Equal(aFPs, bFPs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
|
||||
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pubKey := keyRingFromKey(testPublicKey)
|
||||
|
||||
tests := []testData{
|
||||
{
|
||||
@ -88,8 +131,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
contactMeta: nil,
|
||||
isInternal: true,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -107,8 +150,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
contactMeta: nil,
|
||||
isInternal: true,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPInlinePackage,
|
||||
},
|
||||
@ -126,8 +169,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
contactMeta: nil,
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -145,8 +188,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
contactMeta: nil,
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPInlinePackage,
|
||||
},
|
||||
@ -164,8 +207,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
contactMeta: nil,
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: false,
|
||||
settingsPgpScheme: pmapi.PGPInlinePackage,
|
||||
},
|
||||
@ -183,8 +226,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
eventListener: m.eventListener,
|
||||
contactMeta: nil,
|
||||
isInternal: true,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{pubKey},
|
||||
},
|
||||
wantSendingInfo: SendingInfo{},
|
||||
wantErr: true,
|
||||
@ -195,8 +238,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
contactMeta: nil,
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -217,20 +260,11 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
|
||||
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pubKey := keyRingFromKey(testPublicKey)
|
||||
|
||||
preferredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
preferredPubKey := keyRingFromKey(testPublicKey)
|
||||
|
||||
differentPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testDifferentPublicKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
differentPubKey := keyRingFromKey(testDifferentPublicKey)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.NoActiveKeyForRecipientEvent, "badkey@email.com")
|
||||
|
||||
@ -241,8 +275,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
|
||||
isInternal: true,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -260,8 +294,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
|
||||
isInternal: true,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.KeyRing{pubKey},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -279,8 +313,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
|
||||
isInternal: true,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{preferredPubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
apiKeys: []*crypto.KeyRing{preferredPubKey},
|
||||
contactKeys: []*crypto.KeyRing{pubKey},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -299,8 +333,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Email: "badkey@email.com", Encrypt: true, Scheme: "pgp-mime"},
|
||||
isInternal: true,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{differentPubKey},
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.KeyRing{differentPubKey},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -313,8 +347,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -332,8 +366,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
contactKeys: []*pmcrypto.KeyRing{differentPubKey},
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.KeyRing{differentPubKey},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -352,15 +386,9 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pubKey := keyRingFromKey(testPublicKey)
|
||||
|
||||
expiredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testExpiredPublicKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
expiredPubKey := keyRingFromKey(testExpiredPublicKey)
|
||||
|
||||
tests := []testData{
|
||||
{
|
||||
@ -369,8 +397,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -388,8 +416,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{expiredPubKey},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{expiredPubKey},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -407,8 +435,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{pubKey},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -426,8 +454,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-inline"},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{pubKey},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -445,8 +473,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Encrypt: true},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{pubKey},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{pubKey},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPMIMEPackage,
|
||||
},
|
||||
@ -464,8 +492,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPInlinePackage,
|
||||
},
|
||||
@ -483,8 +511,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{MIMEType: pmapi.ContentTypePlainText},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPInlinePackage,
|
||||
},
|
||||
@ -502,8 +530,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{SignMissing: true},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*pmcrypto.KeyRing{},
|
||||
contactKeys: []*pmcrypto.KeyRing{},
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.KeyRing{},
|
||||
settingsSign: true,
|
||||
settingsPgpScheme: pmapi.PGPInlinePackage,
|
||||
},
|
||||
|
||||
@ -18,8 +18,8 @@
|
||||
// Package smtp provides SMTP server of the Bridge.
|
||||
package smtp
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
var (
|
||||
log = config.GetLogEntry("smtp") //nolint[gochecknoglobals]
|
||||
log = logrus.WithField("pkg", "smtp") //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
@ -20,13 +20,13 @@ package smtp
|
||||
import (
|
||||
"io"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type storeUserProvider interface {
|
||||
CreateDraft(
|
||||
kr *pmcrypto.KeyRing,
|
||||
kr *crypto.KeyRing,
|
||||
message *pmapi.Message,
|
||||
attachmentReaders []io.Reader,
|
||||
attachedPublicKey,
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -32,8 +31,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
@ -47,7 +45,6 @@ type smtpUser struct {
|
||||
eventListener listener.Listener
|
||||
backend *smtpBackend
|
||||
user bridgeUser
|
||||
client bridge.PMAPIProvider
|
||||
storeUser storeUserProvider
|
||||
addressID string
|
||||
}
|
||||
@ -60,9 +57,6 @@ func newSMTPUser(
|
||||
user bridgeUser,
|
||||
addressID string,
|
||||
) (goSMTPBackend.User, error) {
|
||||
// Using client directly is deprecated. Code should be moved to store.
|
||||
client := user.GetTemporaryPMAPIClient()
|
||||
|
||||
storeUser := user.GetStore()
|
||||
if storeUser == nil {
|
||||
return nil, errors.New("user database is not initialized")
|
||||
@ -73,37 +67,51 @@ func newSMTPUser(
|
||||
eventListener: eventListener,
|
||||
backend: smtpBackend,
|
||||
user: user,
|
||||
client: client,
|
||||
storeUser: storeUser,
|
||||
addressID: addressID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This method should eventually no longer be necessary. Everything should go via store.
|
||||
func (su *smtpUser) client() pmapi.Client {
|
||||
return su.user.GetTemporaryPMAPIClient()
|
||||
}
|
||||
|
||||
// Send sends an email from the given address to the given addresses with the given body.
|
||||
func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err error) { //nolint[funlen]
|
||||
// Called from go-smtp in goroutines - we need to handle panics for each function.
|
||||
defer su.panicHandler.HandlePanic()
|
||||
|
||||
mailSettings, err := su.client.GetMailSettings()
|
||||
mailSettings, err := su.client().GetMailSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var addr *pmapi.Address = su.client.Addresses().ByEmail(from)
|
||||
var addr *pmapi.Address = su.client().Addresses().ByEmail(from)
|
||||
if addr == nil {
|
||||
err = errors.New("backend: invalid email address: not owned by user")
|
||||
return
|
||||
}
|
||||
kr := addr.KeyRing()
|
||||
|
||||
kr, err := su.client().KeyRingForAddressID(addr.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var attachedPublicKey string
|
||||
var attachedPublicKeyName string
|
||||
if mailSettings.AttachPublicKey > 0 {
|
||||
attachedPublicKey, err = kr.GetArmoredPublicKey()
|
||||
firstKey, err := kr.GetKey(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attachedPublicKeyName = "publickey - " + kr.Identities()[0].Name
|
||||
|
||||
attachedPublicKey, err = firstKey.GetArmoredPublicKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attachedPublicKeyName = "publickey - " + kr.GetIdentities()[0].Name
|
||||
}
|
||||
|
||||
message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName)
|
||||
@ -139,11 +147,11 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
// but it's better than sending the message many times. If the message was sent, we simply return
|
||||
// nil to indicate it's OK.
|
||||
sendRecorderMessageHash := su.backend.sendRecorder.getMessageHash(message)
|
||||
isSending, wasSent := su.backend.sendRecorder.isSendingOrSent(su.client, sendRecorderMessageHash)
|
||||
isSending, wasSent := su.backend.sendRecorder.isSendingOrSent(su.client(), sendRecorderMessageHash)
|
||||
if isSending {
|
||||
log.Debug("Message is in send queue, waiting")
|
||||
time.Sleep(60 * time.Second)
|
||||
isSending, wasSent = su.backend.sendRecorder.isSendingOrSent(su.client, sendRecorderMessageHash)
|
||||
isSending, wasSent = su.backend.sendRecorder.isSendingOrSent(su.client(), sendRecorderMessageHash)
|
||||
}
|
||||
if isSending {
|
||||
log.Debug("Message is still in send queue, returning error")
|
||||
@ -165,14 +173,14 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
// can lead to sending the wrong message. Also clients do not necessarily
|
||||
// delete the old draft.
|
||||
if draftID != "" {
|
||||
if err := su.client.DeleteMessages([]string{draftID}); err != nil {
|
||||
if err := su.client().DeleteMessages([]string{draftID}); err != nil {
|
||||
log.WithError(err).WithField("draftID", draftID).Warn("Original draft cannot be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
atts = append(atts, message.Attachments...)
|
||||
// Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys.
|
||||
attkeys := make(map[string]*pmcrypto.SymmetricKey)
|
||||
attkeys := make(map[string]*crypto.SessionKey)
|
||||
attkeysEncoded := make(map[string]pmapi.AlgoKey)
|
||||
|
||||
for _, att := range atts {
|
||||
@ -204,28 +212,32 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
// PMEL 3.
|
||||
composeMode := message.MIMEType
|
||||
|
||||
var plainKey, htmlKey, mimeKey *pmcrypto.SymmetricKey
|
||||
var plainKey, htmlKey, mimeKey *crypto.SessionKey
|
||||
var plainData, htmlData, mimeData []byte
|
||||
|
||||
containsUnencryptedRecipients := false
|
||||
|
||||
for _, email := range to {
|
||||
if !looksLikeEmail(email) {
|
||||
return errors.New(`"` + email + `" is not a valid recipient.`)
|
||||
}
|
||||
|
||||
// PMEL 1.
|
||||
contactEmails, err := su.client.GetContactEmailByEmail(email, 0, 1000)
|
||||
contactEmails, err := su.client().GetContactEmailByEmail(email, 0, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contactMeta *ContactMetadata
|
||||
var contactKeys []*pmcrypto.KeyRing
|
||||
var contactKeyRings []*crypto.KeyRing
|
||||
for _, contactEmail := range contactEmails {
|
||||
if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_
|
||||
continue
|
||||
}
|
||||
contact, err := su.client.GetContactByID(contactEmail.ContactID)
|
||||
contact, err := su.client().GetContactByID(contactEmail.ContactID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCards, err := su.client.DecryptAndVerifyCards(contact.Cards)
|
||||
decryptedCards, err := su.client().DecryptAndVerifyCards(contact.Cards)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -233,34 +245,47 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, contactRawKey := range contactMeta.Keys {
|
||||
contactKey, err := pmcrypto.ReadKeyRing(bytes.NewBufferString(contactRawKey))
|
||||
contactKeyRing, err := crypto.NewKeyRing(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contactKeys = append(contactKeys, contactKey)
|
||||
for _, contactRawKey := range contactMeta.Keys {
|
||||
contactKey, err := crypto.NewKey([]byte(contactRawKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := contactKeyRing.AddKey(contactKey); err != nil {
|
||||
return err
|
||||
}
|
||||
contactKeyRings = append(contactKeyRings, contactKeyRing)
|
||||
}
|
||||
|
||||
break // We take the first hit where Defaults == 0, see "How to find the right contact" of PMEL
|
||||
}
|
||||
|
||||
// PMEL 4.
|
||||
apiRawKeyList, isInternal, err := su.client.GetPublicKeysForEmail(email)
|
||||
apiRawKeyList, isInternal, err := su.client().GetPublicKeysForEmail(email)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("backend: cannot get recipients' public keys: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var apiKeys []*pmcrypto.KeyRing
|
||||
var apiKeyRings []*crypto.KeyRing
|
||||
for _, apiRawKey := range apiRawKeyList {
|
||||
var kr *pmcrypto.KeyRing
|
||||
if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(apiRawKey.PublicKey)); err != nil {
|
||||
key, err := crypto.NewKeyFromArmored(apiRawKey.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiKeys = append(apiKeys, kr)
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme)
|
||||
apiKeyRings = append(apiKeyRings, kr)
|
||||
}
|
||||
|
||||
sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeyRings, contactKeyRings, settingsSign, settingsPgpScheme)
|
||||
if !sendingInfo.Encrypt {
|
||||
containsUnencryptedRecipients = true
|
||||
}
|
||||
@ -281,7 +306,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
}
|
||||
}
|
||||
if sendingInfo.Scheme == pmapi.PGPMIMEPackage {
|
||||
mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*pmcrypto.SymmetricKey{})
|
||||
mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*crypto.SessionKey{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -333,7 +358,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
return errors.New("error decoding subject message " + message.Header.Get("Subject"))
|
||||
}
|
||||
if !su.continueSendingUnencryptedMail(subject) {
|
||||
_ = su.client.DeleteMessages([]string{message.ID})
|
||||
_ = su.client().DeleteMessages([]string{message.ID})
|
||||
return errors.New("sending was canceled by user")
|
||||
}
|
||||
}
|
||||
@ -371,19 +396,19 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
|
||||
references := m.Header.Get("References")
|
||||
newReferences := []string{}
|
||||
for _, reference := range strings.Fields(references) {
|
||||
if !strings.Contains(reference, "@protonmail.internalid") {
|
||||
if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) {
|
||||
newReferences = append(newReferences, reference)
|
||||
} else { // internalid is the parentID.
|
||||
idMatch := regexp.MustCompile("[a-zA-Z0-9-_=]*@protonmail.internalid").FindString(reference)
|
||||
if idMatch != "" {
|
||||
lastID := idMatch[0 : len(idMatch)-len("@protonmail.internalid")]
|
||||
idMatch := regexp.MustCompile(pmapi.InternalReferenceFormat).FindStringSubmatch(reference)
|
||||
if len(idMatch) > 0 {
|
||||
lastID := strings.TrimSuffix(strings.Trim(idMatch[0], "<>"), "@protonmail.internalid")
|
||||
filter := &pmapi.MessagesFilter{ID: []string{lastID}}
|
||||
if su.addressID != "" {
|
||||
filter.AddressID = su.addressID
|
||||
}
|
||||
metadata, _, _ := su.client.ListMessages(filter)
|
||||
metadata, _, _ := su.client().ListMessages(filter)
|
||||
for _, m := range metadata {
|
||||
if isDraft(m) {
|
||||
if m.IsDraft() {
|
||||
draftID = m.ID
|
||||
} else {
|
||||
parentID = m.ID
|
||||
@ -401,7 +426,7 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
|
||||
if su.addressID != "" {
|
||||
filter.AddressID = su.addressID
|
||||
}
|
||||
metadata, _, _ := su.client.ListMessages(filter)
|
||||
metadata, _, _ := su.client().ListMessages(filter)
|
||||
// There can be two or messages with the same external ID and then we cannot
|
||||
// be sure which message should be parent. Better to not choose any.
|
||||
if len(metadata) == 1 {
|
||||
@ -412,15 +437,6 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
|
||||
return draftID, parentID
|
||||
}
|
||||
|
||||
func isDraft(m *pmapi.Message) bool {
|
||||
for _, labelID := range m.LabelIDs {
|
||||
if labelID == pmapi.DraftLabel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (su *smtpUser) handleSenderAndRecipients(m *pmapi.Message, addr *pmapi.Address, from string, to []string) (err error) {
|
||||
from = pmapi.ConstructAddress(from, addr.Email)
|
||||
|
||||
|
||||
@ -19,15 +19,27 @@ package smtp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"regexp"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals // Used like a constant
|
||||
var mailFormat = regexp.MustCompile(`.+@.+\..+`)
|
||||
|
||||
// looksLikeEmail validates whether the string resembles an email.
|
||||
//
|
||||
// Notice that it does this naively by simply checking for the existence
|
||||
// of a DOT and an AT sign.
|
||||
func looksLikeEmail(e string) bool {
|
||||
return mailFormat.MatchString(e)
|
||||
}
|
||||
|
||||
func createPackets(
|
||||
pubkey *pmcrypto.KeyRing,
|
||||
bodyKey *pmcrypto.SymmetricKey,
|
||||
attkeys map[string]*pmcrypto.SymmetricKey,
|
||||
pubkey *crypto.KeyRing,
|
||||
bodyKey *crypto.SessionKey,
|
||||
attkeys map[string]*crypto.SessionKey,
|
||||
) (bodyPacket string, attachmentPackets map[string]string, err error) {
|
||||
// Encrypt message body keys.
|
||||
packetBytes, err := pubkey.EncryptSessionKey(bodyKey)
|
||||
@ -49,24 +61,33 @@ func createPackets(
|
||||
}
|
||||
|
||||
func encryptSymmetric(
|
||||
kr *pmcrypto.KeyRing,
|
||||
kr *crypto.KeyRing,
|
||||
textToEncrypt string,
|
||||
canonicalizeText bool, // nolint[unparam]
|
||||
) (key *pmcrypto.SymmetricKey, symEncryptedData []byte, err error) {
|
||||
) (key *crypto.SessionKey, symEncryptedData []byte, err error) {
|
||||
// We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones).
|
||||
pgpMessage, err := kr.FirstKey().Encrypt(pmcrypto.NewPlainMessageFromString(textToEncrypt), kr)
|
||||
firstKey, err := kr.FirstKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
symEncryptedData = pgpSplitMessage.GetBinaryDataPacket()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -75,7 +96,7 @@ func buildPackage(
|
||||
sharedScheme int,
|
||||
mimeType string,
|
||||
bodyData []byte,
|
||||
bodyKey *pmcrypto.SymmetricKey,
|
||||
bodyKey *crypto.SessionKey,
|
||||
attKeys map[string]pmapi.AlgoKey,
|
||||
) (pkg *pmapi.MessagePackage) {
|
||||
if len(addressMap) == 0 {
|
||||
|
||||
@ -109,5 +109,9 @@ func (storeAddress *Address) AddressID() string {
|
||||
|
||||
// APIAddress returns the `pmapi.Address` struct.
|
||||
func (storeAddress *Address) APIAddress() *pmapi.Address {
|
||||
return storeAddress.store.api.Addresses().ByEmail(storeAddress.address)
|
||||
return storeAddress.client().Addresses().ByEmail(storeAddress.address)
|
||||
}
|
||||
|
||||
func (storeAddress *Address) client() pmapi.Client {
|
||||
return storeAddress.store.client()
|
||||
}
|
||||
|
||||
@ -78,6 +78,7 @@ func (storeAddress *Address) createOrUpdateMailboxEvent(label *pmapi.Label) erro
|
||||
return err
|
||||
}
|
||||
storeAddress.mailboxes[label.ID] = mailbox
|
||||
mailbox.store.imapMailboxCreated(storeAddress.address, mailbox.labelName)
|
||||
} else {
|
||||
mailbox.labelName = prefix + label.Name
|
||||
mailbox.color = label.Color
|
||||
|
||||
@ -29,7 +29,7 @@ import (
|
||||
|
||||
// SetIMAPUpdateChannel sets the channel on which imap update messages will be sent. This should be the channel
|
||||
// on which the imap backend listens for imap updates.
|
||||
func (store *Store) SetIMAPUpdateChannel(updates chan interface{}) {
|
||||
func (store *Store) SetIMAPUpdateChannel(updates chan imapBackend.Update) {
|
||||
store.log.Debug("Listening for IMAP updates")
|
||||
|
||||
if store.imapUpdates = updates; store.imapUpdates == nil {
|
||||
@ -39,9 +39,9 @@ func (store *Store) SetIMAPUpdateChannel(updates chan interface{}) {
|
||||
|
||||
func (store *Store) imapNotice(address, notice string) {
|
||||
update := new(imapBackend.StatusUpdate)
|
||||
update.Username = address
|
||||
update.Update = imapBackend.NewUpdate(address, "")
|
||||
update.StatusResp = &imap.StatusResp{
|
||||
Type: imap.StatusOk,
|
||||
Type: imap.StatusRespOk,
|
||||
Code: imap.CodeAlert,
|
||||
Info: notice,
|
||||
}
|
||||
@ -57,9 +57,8 @@ func (store *Store) imapUpdateMessage(address, mailboxName string, uid, sequence
|
||||
"flags": message.GetFlags(msg),
|
||||
}).Trace("IDLE update")
|
||||
update := new(imapBackend.MessageUpdate)
|
||||
update.Username = address
|
||||
update.Mailbox = mailboxName
|
||||
update.Message = imap.NewMessage(sequenceNumber, []string{imap.FlagsMsgAttr, imap.UidMsgAttr})
|
||||
update.Update = imapBackend.NewUpdate(address, mailboxName)
|
||||
update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
|
||||
update.Message.Flags = message.GetFlags(msg)
|
||||
update.Message.Uid = uid
|
||||
store.imapSendUpdate(update)
|
||||
@ -72,29 +71,44 @@ func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumbe
|
||||
"seqNum": sequenceNumber,
|
||||
}).Trace("IDLE delete")
|
||||
update := new(imapBackend.ExpungeUpdate)
|
||||
update.Username = address
|
||||
update.Mailbox = mailboxName
|
||||
update.Update = imapBackend.NewUpdate(address, mailboxName)
|
||||
update.SeqNum = sequenceNumber
|
||||
store.imapSendUpdate(update)
|
||||
}
|
||||
|
||||
func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread uint) {
|
||||
func (store *Store) imapMailboxCreated(address, mailboxName string) {
|
||||
store.log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
}).Trace("IDLE mailbox info")
|
||||
update := new(imapBackend.MailboxInfoUpdate)
|
||||
update.Update = imapBackend.NewUpdate(address, "")
|
||||
update.MailboxInfo = &imap.MailboxInfo{
|
||||
Attributes: []string{imap.NoInferiorsAttr},
|
||||
Delimiter: PathDelimiter,
|
||||
Name: mailboxName,
|
||||
}
|
||||
store.imapSendUpdate(update)
|
||||
}
|
||||
|
||||
func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) {
|
||||
store.log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
"total": total,
|
||||
"unread": unread,
|
||||
"unreadSeqNum": unreadSeqNum,
|
||||
}).Trace("IDLE status")
|
||||
update := new(imapBackend.MailboxUpdate)
|
||||
update.Username = address
|
||||
update.Mailbox = mailboxName
|
||||
update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []string{imap.MailboxMessages, imap.MailboxUnseen})
|
||||
update.Update = imapBackend.NewUpdate(address, mailboxName)
|
||||
update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen})
|
||||
update.MailboxStatus.Messages = uint32(total)
|
||||
update.MailboxStatus.Unseen = uint32(unread)
|
||||
update.MailboxStatus.UnseenSeqNum = uint32(unreadSeqNum)
|
||||
store.imapSendUpdate(update)
|
||||
}
|
||||
|
||||
func (store *Store) imapSendUpdate(update interface{}) {
|
||||
func (store *Store) imapSendUpdate(update imapBackend.Update) {
|
||||
if store.imapUpdates == nil {
|
||||
store.log.Trace("IMAP IDLE unavailable")
|
||||
return
|
||||
|
||||
@ -29,7 +29,7 @@ func TestCreateOrUpdateMessageIMAPUpdates(t *testing.T) {
|
||||
m, clear := initMocks(t)
|
||||
defer clear()
|
||||
|
||||
updates := make(chan interface{})
|
||||
updates := make(chan imapBackend.Update)
|
||||
|
||||
m.newStoreNoEvents(true)
|
||||
m.store.SetIMAPUpdateChannel(updates)
|
||||
@ -49,7 +49,7 @@ func TestCreateOrUpdateMessageIMAPUpdatesBulkUpdate(t *testing.T) {
|
||||
m, clear := initMocks(t)
|
||||
defer clear()
|
||||
|
||||
updates := make(chan interface{})
|
||||
updates := make(chan imapBackend.Update)
|
||||
|
||||
m.newStoreNoEvents(true)
|
||||
m.store.SetIMAPUpdateChannel(updates)
|
||||
@ -75,7 +75,7 @@ func TestDeleteMessageIMAPUpdate(t *testing.T) {
|
||||
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel})
|
||||
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel})
|
||||
|
||||
updates := make(chan interface{})
|
||||
updates := make(chan imapBackend.Update)
|
||||
m.store.SetIMAPUpdateChannel(updates)
|
||||
go checkIMAPUpdates(t, updates, []func(interface{}) bool{
|
||||
checkMessageDelete(addr1, "All Mail", 2),
|
||||
@ -87,7 +87,7 @@ func TestDeleteMessageIMAPUpdate(t *testing.T) {
|
||||
close(updates)
|
||||
}
|
||||
|
||||
func checkIMAPUpdates(t *testing.T, updates chan interface{}, checkFunctions []func(interface{}) bool) {
|
||||
func checkIMAPUpdates(t *testing.T, updates chan imapBackend.Update, checkFunctions []func(interface{}) bool) {
|
||||
idx := 0
|
||||
for update := range updates {
|
||||
if idx >= len(checkFunctions) {
|
||||
@ -105,8 +105,8 @@ func checkMessageUpdate(username, mailbox string, seqNum, uid int) func(interfac
|
||||
return func(update interface{}) bool {
|
||||
switch u := update.(type) {
|
||||
case *imapBackend.MessageUpdate:
|
||||
return (u.Update.Username == username &&
|
||||
u.Update.Mailbox == mailbox &&
|
||||
return (u.Update.Username() == username &&
|
||||
u.Update.Mailbox() == mailbox &&
|
||||
u.Message.SeqNum == uint32(seqNum) &&
|
||||
u.Message.Uid == uint32(uid))
|
||||
default:
|
||||
@ -119,8 +119,8 @@ func checkMessageDelete(username, mailbox string, seqNum int) func(interface{})
|
||||
return func(update interface{}) bool {
|
||||
switch u := update.(type) {
|
||||
case *imapBackend.ExpungeUpdate:
|
||||
return (u.Update.Username == username &&
|
||||
u.Update.Mailbox == mailbox &&
|
||||
return (u.Update.Username() == username &&
|
||||
u.Update.Mailbox() == mailbox &&
|
||||
u.SeqNum == uint32(seqNum))
|
||||
default:
|
||||
return false
|
||||
|
||||
65
internal/store/cooldown.go
Normal file
65
internal/store/cooldown.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package store
|
||||
|
||||
import "time"
|
||||
|
||||
type cooldown struct {
|
||||
waitTimes []time.Duration
|
||||
waitIndex int
|
||||
lastTry time.Time
|
||||
}
|
||||
|
||||
func (c *cooldown) setExponentialWait(initial time.Duration, base int, maximum time.Duration) {
|
||||
waitTimes := []time.Duration{}
|
||||
t := initial
|
||||
if base > 1 {
|
||||
for t < maximum {
|
||||
waitTimes = append(waitTimes, t)
|
||||
t *= time.Duration(base)
|
||||
}
|
||||
}
|
||||
waitTimes = append(waitTimes, maximum)
|
||||
c.setWaitTimes(waitTimes...)
|
||||
}
|
||||
|
||||
func (c *cooldown) setWaitTimes(newTimes ...time.Duration) {
|
||||
c.waitTimes = newTimes
|
||||
c.reset()
|
||||
}
|
||||
|
||||
// isTooSoon™ returns whether the cooldown period is not yet over.
|
||||
func (c *cooldown) isTooSoon() bool {
|
||||
if time.Since(c.lastTry) < c.waitTimes[c.waitIndex] {
|
||||
return true
|
||||
}
|
||||
c.lastTry = time.Now()
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *cooldown) increaseWaitTime() {
|
||||
c.lastTry = time.Now()
|
||||
if c.waitIndex+1 < len(c.waitTimes) {
|
||||
c.waitIndex++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cooldown) reset() {
|
||||
c.waitIndex = 0
|
||||
c.lastTry = time.Time{}
|
||||
}
|
||||
132
internal/store/cooldown_test.go
Normal file
132
internal/store/cooldown_test.go
Normal file
@ -0,0 +1,132 @@
|
||||
// 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 store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCooldownExponentialWait(t *testing.T) {
|
||||
ms := time.Millisecond
|
||||
sec := time.Second
|
||||
|
||||
testData := []struct {
|
||||
haveInitial, haveMax time.Duration
|
||||
haveBase int
|
||||
wantWaitTimes []time.Duration
|
||||
}{
|
||||
{
|
||||
haveInitial: 1 * sec,
|
||||
haveBase: 0,
|
||||
haveMax: 0 * sec,
|
||||
wantWaitTimes: []time.Duration{0 * sec},
|
||||
},
|
||||
{
|
||||
haveInitial: 0 * sec,
|
||||
haveBase: 1,
|
||||
haveMax: 0 * sec,
|
||||
wantWaitTimes: []time.Duration{0 * sec},
|
||||
},
|
||||
{
|
||||
haveInitial: 0 * sec,
|
||||
haveBase: 0,
|
||||
haveMax: 1 * sec,
|
||||
wantWaitTimes: []time.Duration{1 * sec},
|
||||
},
|
||||
{
|
||||
haveInitial: 0 * sec,
|
||||
haveBase: 1,
|
||||
haveMax: 1 * sec,
|
||||
wantWaitTimes: []time.Duration{1 * sec},
|
||||
},
|
||||
{
|
||||
haveInitial: 1 * sec,
|
||||
haveBase: 0,
|
||||
haveMax: 1 * sec,
|
||||
wantWaitTimes: []time.Duration{1 * sec},
|
||||
},
|
||||
{
|
||||
haveInitial: 1 * sec,
|
||||
haveBase: 2,
|
||||
haveMax: 1 * sec,
|
||||
wantWaitTimes: []time.Duration{1 * sec},
|
||||
},
|
||||
{
|
||||
haveInitial: 500 * ms,
|
||||
haveBase: 2,
|
||||
haveMax: 5 * sec,
|
||||
wantWaitTimes: []time.Duration{500 * ms, 1 * sec, 2 * sec, 4 * sec, 5 * sec},
|
||||
},
|
||||
}
|
||||
|
||||
var testCooldown cooldown
|
||||
|
||||
for _, td := range testData {
|
||||
testCooldown.setExponentialWait(td.haveInitial, td.haveBase, td.haveMax)
|
||||
assert.Equal(t, td.wantWaitTimes, testCooldown.waitTimes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCooldownIncreaseAndReset(t *testing.T) {
|
||||
var testCooldown cooldown
|
||||
testCooldown.setWaitTimes(1*time.Second, 2*time.Second, 3*time.Second)
|
||||
assert.Equal(t, 0, testCooldown.waitIndex)
|
||||
|
||||
assert.False(t, testCooldown.isTooSoon())
|
||||
assert.True(t, testCooldown.isTooSoon())
|
||||
assert.Equal(t, 0, testCooldown.waitIndex)
|
||||
|
||||
testCooldown.reset()
|
||||
assert.Equal(t, 0, testCooldown.waitIndex)
|
||||
|
||||
assert.False(t, testCooldown.isTooSoon())
|
||||
assert.True(t, testCooldown.isTooSoon())
|
||||
assert.Equal(t, 0, testCooldown.waitIndex)
|
||||
|
||||
// increase at least N+1 times to check overflow
|
||||
testCooldown.increaseWaitTime()
|
||||
assert.True(t, testCooldown.isTooSoon())
|
||||
testCooldown.increaseWaitTime()
|
||||
assert.True(t, testCooldown.isTooSoon())
|
||||
testCooldown.increaseWaitTime()
|
||||
assert.True(t, testCooldown.isTooSoon())
|
||||
testCooldown.increaseWaitTime()
|
||||
assert.True(t, testCooldown.isTooSoon())
|
||||
|
||||
assert.Equal(t, 2, testCooldown.waitIndex)
|
||||
}
|
||||
|
||||
func TestCooldownNotSooner(t *testing.T) {
|
||||
var testCooldown cooldown
|
||||
waitTime := 100 * time.Millisecond
|
||||
testCooldown.setWaitTimes(waitTime)
|
||||
|
||||
// First time it should never be too soon.
|
||||
assert.False(t, testCooldown.isTooSoon())
|
||||
|
||||
// Only half of given wait time should be too soon.
|
||||
time.Sleep(waitTime / 2)
|
||||
assert.True(t, testCooldown.isTooSoon())
|
||||
|
||||
// After given wait time it shouldn't be soon anymore.
|
||||
time.Sleep(waitTime / 2)
|
||||
assert.False(t, testCooldown.isTooSoon())
|
||||
}
|
||||
@ -18,6 +18,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
bridgeEvents "github.com/ProtonMail/proton-bridge/internal/events"
|
||||
@ -28,25 +29,28 @@ import (
|
||||
)
|
||||
|
||||
const pollInterval = 30 * time.Second
|
||||
const pollIntervalSpread = 5 * time.Second
|
||||
|
||||
type eventLoop struct {
|
||||
cache *Cache
|
||||
currentEventID string
|
||||
currentEvent *pmapi.Event
|
||||
pollCh chan chan struct{}
|
||||
stopCh chan struct{}
|
||||
notifyStopCh chan struct{}
|
||||
isRunning bool
|
||||
hasInternet bool
|
||||
|
||||
pollCounter int
|
||||
|
||||
log *logrus.Entry
|
||||
|
||||
store *Store
|
||||
apiClient PMAPIProvider
|
||||
user BridgeUser
|
||||
events listener.Listener
|
||||
}
|
||||
|
||||
func newEventLoop(cache *Cache, store *Store, api PMAPIProvider, user BridgeUser, events listener.Listener) *eventLoop {
|
||||
func newEventLoop(cache *Cache, store *Store, user BridgeUser, events listener.Listener) *eventLoop {
|
||||
eventLog := log.WithField("userID", user.ID())
|
||||
eventLog.Trace("Creating new event loop")
|
||||
|
||||
@ -59,7 +63,6 @@ func newEventLoop(cache *Cache, store *Store, api PMAPIProvider, user BridgeUser
|
||||
log: eventLog,
|
||||
|
||||
store: store,
|
||||
apiClient: api,
|
||||
user: user,
|
||||
events: events,
|
||||
}
|
||||
@ -69,10 +72,14 @@ func (loop *eventLoop) IsRunning() bool {
|
||||
return loop.isRunning
|
||||
}
|
||||
|
||||
func (loop *eventLoop) setFirstEventID() (err error) {
|
||||
loop.log.Trace("Setting first event ID")
|
||||
func (loop *eventLoop) client() pmapi.Client {
|
||||
return loop.store.client()
|
||||
}
|
||||
|
||||
event, err := loop.apiClient.GetEvent("")
|
||||
func (loop *eventLoop) setFirstEventID() (err error) {
|
||||
loop.log.Info("Setting first event ID")
|
||||
|
||||
event, err := loop.client().GetEvent("")
|
||||
if err != nil {
|
||||
loop.log.WithError(err).Error("Could not get latest event ID")
|
||||
return
|
||||
@ -104,14 +111,14 @@ func (loop *eventLoop) stop() {
|
||||
|
||||
select {
|
||||
case <-loop.notifyStopCh:
|
||||
loop.log.Info("Event loop was stopped")
|
||||
loop.log.Warn("Event loop was stopped")
|
||||
case <-time.After(1 * time.Second):
|
||||
loop.log.Warn("Timed out waiting for event loop to stop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (loop *eventLoop) start() { // nolint[funlen]
|
||||
func (loop *eventLoop) start() {
|
||||
if loop.isRunning {
|
||||
return
|
||||
}
|
||||
@ -127,24 +134,32 @@ func (loop *eventLoop) start() { // nolint[funlen]
|
||||
|
||||
loop.log.WithField("lastEventID", loop.currentEventID).Info("Subscribed to events")
|
||||
defer func() {
|
||||
loop.log.WithField("lastEventID", loop.currentEventID).Info("Subscription stopped")
|
||||
loop.log.WithField("lastEventID", loop.currentEventID).Warn("Subscription stopped")
|
||||
}()
|
||||
|
||||
t := time.NewTicker(pollInterval)
|
||||
defer t.Stop()
|
||||
|
||||
loop.hasInternet = true
|
||||
|
||||
go loop.pollNow()
|
||||
|
||||
loop.loop()
|
||||
}
|
||||
|
||||
// loop is the main body of the event loop.
|
||||
func (loop *eventLoop) loop() {
|
||||
t := time.NewTicker(pollInterval - pollIntervalSpread)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
var eventProcessedCh chan struct{}
|
||||
select {
|
||||
case <-loop.stopCh:
|
||||
close(loop.notifyStopCh)
|
||||
return
|
||||
case eventProcessedCh = <-loop.pollCh:
|
||||
case <-t.C:
|
||||
// Randomise periodic calls within range pollInterval ± pollSpread to reduces potential load spikes on API.
|
||||
time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond)
|
||||
case eventProcessedCh = <-loop.pollCh:
|
||||
// We don't want to wait here. Polling should happen instantly.
|
||||
}
|
||||
|
||||
// Before we fetch the first event, check whether this is the first time we've
|
||||
@ -194,7 +209,9 @@ func (loop *eventLoop) isBeforeFirstStart() bool {
|
||||
// (disk). It will filter out in defer all errors except invalid token error.
|
||||
// Invalid error will be returned and stop the event loop.
|
||||
func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[funlen]
|
||||
l := loop.log.WithField("currentEventID", loop.currentEventID)
|
||||
l := loop.log.
|
||||
WithField("currentEventID", loop.currentEventID).
|
||||
WithField("pollCounter", loop.pollCounter)
|
||||
|
||||
// We only want to consider invalid tokens as real errors because all other errors might fix themselves eventually
|
||||
// (e.g. no internet, ulimit reached etc.)
|
||||
@ -222,17 +239,30 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
|
||||
|
||||
// All errors except Invalid Token (which is not possible to recover from) are ignored.
|
||||
if err != nil && !errUnauthorized && errors.Cause(err) != pmapi.ErrInvalidToken {
|
||||
l.WithError(err).Trace("Error skipped")
|
||||
l.WithError(err).Error("Error skipped")
|
||||
err = nil
|
||||
}
|
||||
}()
|
||||
|
||||
l.Trace("Polling next event")
|
||||
// Log activity of event loop each 100. poll which means approx. 28
|
||||
// lines per day
|
||||
if loop.pollCounter%100 == 0 {
|
||||
l.Info("Polling next event")
|
||||
}
|
||||
loop.pollCounter++
|
||||
|
||||
var event *pmapi.Event
|
||||
if event, err = loop.apiClient.GetEvent(loop.currentEventID); err != nil {
|
||||
if event, err = loop.client().GetEvent(loop.currentEventID); err != nil {
|
||||
return false, errors.Wrap(err, "failed to get event")
|
||||
}
|
||||
|
||||
loop.currentEvent = event
|
||||
|
||||
if event == nil {
|
||||
return false, errors.New("received empty event")
|
||||
}
|
||||
|
||||
l = l.WithField("newEventID", event.EventID)
|
||||
|
||||
if !loop.hasInternet {
|
||||
@ -245,6 +275,7 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
|
||||
}
|
||||
|
||||
if loop.currentEventID != event.EventID {
|
||||
l.WithField("newID", event.EventID).Info("New event processed")
|
||||
// In case new event ID cannot be saved to cache, we update it in event loop
|
||||
// anyway and continue processing new events to prevent the loop from repeatedly
|
||||
// processing the same event.
|
||||
@ -309,7 +340,7 @@ func (loop *eventLoop) processAddresses(log *logrus.Entry, addressEvents []*pmap
|
||||
log.Debug("Processing address change event")
|
||||
|
||||
// Get old addresses for comparisons before updating user.
|
||||
oldList := loop.apiClient.Addresses()
|
||||
oldList := loop.client().Addresses()
|
||||
|
||||
if err = loop.user.UpdateUser(); err != nil {
|
||||
if logoutErr := loop.user.Logout(); logoutErr != nil {
|
||||
@ -351,7 +382,7 @@ func (loop *eventLoop) processAddresses(log *logrus.Entry, addressEvents []*pmap
|
||||
}
|
||||
}
|
||||
|
||||
if err = loop.store.createOrUpdateAddressInfo(loop.apiClient.Addresses()); err != nil {
|
||||
if err = loop.store.createOrUpdateAddressInfo(loop.client().Addresses()); err != nil {
|
||||
return errors.Wrap(err, "failed to update address IDs in store")
|
||||
}
|
||||
|
||||
@ -382,7 +413,7 @@ func (loop *eventLoop) processLabels(eventLog *logrus.Entry, labels []*pmapi.Eve
|
||||
return nil
|
||||
}
|
||||
|
||||
func (loop *eventLoop) processMessages(eventLog *logrus.Entry, messages []*pmapi.EventMessage) (err error) {
|
||||
func (loop *eventLoop) processMessages(eventLog *logrus.Entry, messages []*pmapi.EventMessage) (err error) { // nolint[funlen]
|
||||
eventLog.Debug("Processing message change event")
|
||||
|
||||
for _, message := range messages {
|
||||
@ -394,7 +425,7 @@ func (loop *eventLoop) processMessages(eventLog *logrus.Entry, messages []*pmapi
|
||||
|
||||
if message.Created == nil {
|
||||
msgLog.Error("Got EventCreate with nil message")
|
||||
break
|
||||
continue
|
||||
}
|
||||
|
||||
if err = loop.store.createOrUpdateMessageEvent(message.Created); err != nil {
|
||||
@ -405,23 +436,28 @@ func (loop *eventLoop) processMessages(eventLog *logrus.Entry, messages []*pmapi
|
||||
msgLog.Debug("Processing EventUpdate(Flags) for message")
|
||||
|
||||
if message.Updated == nil {
|
||||
msgLog.Errorf("Got EventUpdate(Flags) with nil message")
|
||||
break
|
||||
msgLog.Error("Got EventUpdate(Flags) with nil message")
|
||||
continue
|
||||
}
|
||||
|
||||
var msg *pmapi.Message
|
||||
msg, err = loop.store.getMessageFromDB(message.ID)
|
||||
if err == ErrNoSuchAPIID {
|
||||
msgLog.WithError(err).Warning("Cannot get message from DB for updating. Trying fetch...")
|
||||
msg, err = loop.store.fetchMessage(message.ID)
|
||||
// If message does not exist anywhere, update event is probably old and off topic - skip it.
|
||||
if err == ErrNoSuchAPIID {
|
||||
msgLog.Warn("Skipping message update, because message does not exist nor in local DB or on API")
|
||||
|
||||
if msg, err = loop.store.getMessageFromDB(message.ID); err != nil {
|
||||
if err != ErrNoSuchAPIID {
|
||||
return errors.Wrap(err, "failed to get message from DB for updating")
|
||||
}
|
||||
|
||||
msgLog.WithError(err).Warning("Message was not present in DB. Trying fetch...")
|
||||
|
||||
if msg, err = loop.client().GetMessage(message.ID); err != nil {
|
||||
if _, ok := err.(*pmapi.ErrUnprocessableEntity); ok {
|
||||
msgLog.WithError(err).Warn("Skipping message update because message exists neither in local DB nor on API")
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "failed to get message from API for updating")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get message from DB for updating")
|
||||
}
|
||||
|
||||
updateMessage(msgLog, msg, message.Updated)
|
||||
@ -487,14 +523,7 @@ func updateMessage(msgLog *logrus.Entry, message *pmapi.Message, updates *pmapi.
|
||||
message.LabelIDs = updates.LabelIDs
|
||||
} else {
|
||||
for _, added := range updates.LabelIDsAdded {
|
||||
hasLabel := false
|
||||
for _, l := range message.LabelIDs {
|
||||
if added == l {
|
||||
hasLabel = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasLabel {
|
||||
if !message.HasLabelID(added) {
|
||||
msgLog.WithField("added", added).Trace("Adding label to message")
|
||||
message.LabelIDs = append(message.LabelIDs, added)
|
||||
}
|
||||
@ -528,7 +557,7 @@ func (loop *eventLoop) processMessageCounts(l *logrus.Entry, messageCounts []*pm
|
||||
return err
|
||||
}
|
||||
if !isSynced {
|
||||
loop.store.triggerSync()
|
||||
log.Error("The counts between DB and API are not matching")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -39,21 +39,21 @@ func TestEventLoopProcessMoreEvents(t *testing.T) {
|
||||
// Doesn't matter which IDs are used.
|
||||
// This test is trying to see whether event loop will immediately process
|
||||
// next event if there is `More` of them.
|
||||
m.api.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{
|
||||
m.client.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{
|
||||
EventID: "event50",
|
||||
More: 1,
|
||||
}, nil),
|
||||
m.api.EXPECT().GetEvent("event50").Return(&pmapi.Event{
|
||||
m.client.EXPECT().GetEvent("event50").Return(&pmapi.Event{
|
||||
EventID: "event70",
|
||||
More: 0,
|
||||
}, nil),
|
||||
m.api.EXPECT().GetEvent("event70").Return(&pmapi.Event{
|
||||
m.client.EXPECT().GetEvent("event70").Return(&pmapi.Event{
|
||||
EventID: "event71",
|
||||
More: 0,
|
||||
}, nil),
|
||||
)
|
||||
m.newStoreNoEvents(true)
|
||||
m.api.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
|
||||
m.client.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
|
||||
|
||||
// Event loop runs in goroutine and will be stopped by deferred mock clearing.
|
||||
go m.store.eventLoop.start()
|
||||
@ -64,7 +64,7 @@ func TestEventLoopProcessMoreEvents(t *testing.T) {
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
// For normal event we need to wait to next polling.
|
||||
time.Sleep(pollInterval)
|
||||
time.Sleep(pollInterval + pollIntervalSpread)
|
||||
require.Eventually(t, func() bool {
|
||||
return m.store.eventLoop.currentEventID == "event71"
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
@ -78,12 +78,12 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) {
|
||||
newSubject := "new subject"
|
||||
|
||||
// First sync will add message with old subject to database.
|
||||
m.api.EXPECT().GetMessage("msg1").Return(&pmapi.Message{
|
||||
m.client.EXPECT().GetMessage("msg1").Return(&pmapi.Message{
|
||||
ID: "msg1",
|
||||
Subject: subject,
|
||||
}, nil)
|
||||
// Event will update the subject.
|
||||
m.api.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{
|
||||
m.client.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{
|
||||
EventID: "event1",
|
||||
Messages: []*pmapi.EventMessage{{
|
||||
EventItem: pmapi.EventItem{
|
||||
|
||||
@ -81,7 +81,7 @@ func syncDraftsIfNecssary(tx *bolt.Tx, mb *Mailbox) { //nolint[funlen]
|
||||
|
||||
// If the drafts mailbox total is non-zero, it means it has already been used
|
||||
// and there is no need to continue. Otherwise, we may need to do an initial sync.
|
||||
total, _, err := mb.txGetCounts(tx)
|
||||
total, _, _, err := mb.txGetCounts(tx)
|
||||
if err != nil || total != 0 {
|
||||
return
|
||||
}
|
||||
@ -258,8 +258,8 @@ func (storeMailbox *Mailbox) pollNow() {
|
||||
}
|
||||
|
||||
// api is a proxy for the store's `PMAPIProvider`.
|
||||
func (storeMailbox *Mailbox) api() PMAPIProvider {
|
||||
return storeMailbox.store.api
|
||||
func (storeMailbox *Mailbox) client() pmapi.Client {
|
||||
return storeMailbox.store.client()
|
||||
}
|
||||
|
||||
// update is a proxy for the store's db's `Update`.
|
||||
|
||||
@ -28,15 +28,15 @@ import (
|
||||
)
|
||||
|
||||
// GetCounts returns numbers of total and unread messages in this mailbox bucket.
|
||||
func (storeMailbox *Mailbox) GetCounts() (total, unread uint, err error) {
|
||||
func (storeMailbox *Mailbox) GetCounts() (total, unread, unseenSeqNum uint, err error) {
|
||||
err = storeMailbox.db().View(func(tx *bolt.Tx) error {
|
||||
total, unread, err = storeMailbox.txGetCounts(tx)
|
||||
total, unread, unseenSeqNum, err = storeMailbox.txGetCounts(tx)
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (storeMailbox *Mailbox) txGetCounts(tx *bolt.Tx) (total, unread uint, err error) {
|
||||
func (storeMailbox *Mailbox) txGetCounts(tx *bolt.Tx) (total, unread, unseenSeqNum uint, err error) {
|
||||
// For total it would be enough to use `bolt.Bucket.Stats().KeyN` but
|
||||
// we also need to retrieve the count of unread emails therefore we are
|
||||
// looping all messages in this mailbox by `bolt.Cursor`
|
||||
@ -48,16 +48,19 @@ func (storeMailbox *Mailbox) txGetCounts(tx *bolt.Tx) (total, unread uint, err e
|
||||
total++
|
||||
rawMsg := metaBucket.Get(apiID)
|
||||
if rawMsg == nil {
|
||||
return 0, 0, ErrNoSuchAPIID
|
||||
return 0, 0, 0, ErrNoSuchAPIID
|
||||
}
|
||||
// Do not unmarshal whole JSON to speed up the looping.
|
||||
// Instead, we assume it will contain JSON int field `Unread`
|
||||
// where `1` means true (i.e. message is unread)
|
||||
if bytes.Contains(rawMsg, []byte(`"Unread":1`)) {
|
||||
if unseenSeqNum == 0 {
|
||||
unseenSeqNum = total
|
||||
}
|
||||
unread++
|
||||
}
|
||||
}
|
||||
return total, unread, err
|
||||
return total, unread, unseenSeqNum, err
|
||||
}
|
||||
|
||||
type mailboxCounts struct {
|
||||
@ -217,7 +220,7 @@ func (store *Store) txGetOnAPICounts(tx *bolt.Tx) ([]*mailboxCounts, error) {
|
||||
|
||||
// createOrUpdateOnAPICounts will change only on-API-counts.
|
||||
func (store *Store) createOrUpdateOnAPICounts(mailboxCountsOnAPI []*pmapi.MessagesCount) error {
|
||||
store.log.WithField("apiCounts", mailboxCountsOnAPI).Debug("Updating API counts")
|
||||
store.log.Debug("Updating API counts")
|
||||
|
||||
tx := func(tx *bolt.Tx) error {
|
||||
countsBkt := tx.Bucket(countsBucket)
|
||||
|
||||
@ -219,6 +219,11 @@ func (storeMailbox *Mailbox) GetUIDByHeader(header *mail.Header) (foundUID uint3
|
||||
// in PM message. Message-Id in normal copy/move will be the PM internal ID.
|
||||
messageID := header.Get("Message-Id")
|
||||
|
||||
// There is nothing to find, when no Message-Id given.
|
||||
if messageID == "" {
|
||||
return uint32(0)
|
||||
}
|
||||
|
||||
// The most often situation is that message is APPENDed after it was sent so the
|
||||
// Message-ID will be reflected by ExternalID in API message meta-data.
|
||||
externalID := strings.Trim(messageID, "<> ") // remove '<>' to improve match
|
||||
|
||||
@ -24,6 +24,8 @@ import (
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder")
|
||||
|
||||
// GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage`
|
||||
// tied to this mailbox.
|
||||
func (storeMailbox *Mailbox) GetMessage(apiID string) (*Message, error) {
|
||||
@ -37,7 +39,7 @@ func (storeMailbox *Mailbox) GetMessage(apiID string) (*Message, error) {
|
||||
// FetchMessage fetches the message with the given `apiID`, stores it in the database, and returns a new store message
|
||||
// wrapping it.
|
||||
func (storeMailbox *Mailbox) FetchMessage(apiID string) (*Message, error) {
|
||||
msg, err := storeMailbox.store.fetchMessage(apiID)
|
||||
msg, err := storeMailbox.client().GetMessage(apiID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -62,7 +64,7 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe
|
||||
LabelIDs: labelIDs,
|
||||
}
|
||||
|
||||
res, err := storeMailbox.api().Import([]*pmapi.ImportMsgReq{importReqs})
|
||||
res, err := storeMailbox.client().Import([]*pmapi.ImportMsgReq{importReqs})
|
||||
if err == nil && len(res) > 0 {
|
||||
msg.ID = res[0].MessageID
|
||||
}
|
||||
@ -78,8 +80,16 @@ func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error {
|
||||
"label": storeMailbox.labelID,
|
||||
"mailbox": storeMailbox.Name,
|
||||
}).Trace("Labeling messages")
|
||||
// Edge case is want to untrash message by drag&drop to AllMail (to not
|
||||
// have it in trash but to not delete message forever). IMAP move would
|
||||
// work okay but some clients might use COPY&EXPUNGE or APPEND&EXPUNGE.
|
||||
// In this case COPY or APPEND is noop because the message is already
|
||||
// in All mail. The consequent EXPUNGE would delete message forever.
|
||||
if storeMailbox.labelID == pmapi.AllMailLabel {
|
||||
return ErrAllMailOpNotAllowed
|
||||
}
|
||||
defer storeMailbox.pollNow()
|
||||
return storeMailbox.api().LabelMessages(apiIDs, storeMailbox.labelID)
|
||||
return storeMailbox.client().LabelMessages(apiIDs, storeMailbox.labelID)
|
||||
}
|
||||
|
||||
// UnlabelMessages removes the label by calling an API.
|
||||
@ -91,8 +101,11 @@ func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error {
|
||||
"label": storeMailbox.labelID,
|
||||
"mailbox": storeMailbox.Name,
|
||||
}).Trace("Unlabeling messages")
|
||||
if storeMailbox.labelID == pmapi.AllMailLabel {
|
||||
return ErrAllMailOpNotAllowed
|
||||
}
|
||||
defer storeMailbox.pollNow()
|
||||
return storeMailbox.api().UnlabelMessages(apiIDs, storeMailbox.labelID)
|
||||
return storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID)
|
||||
}
|
||||
|
||||
// MarkMessagesRead marks the message read by calling an API.
|
||||
@ -116,7 +129,7 @@ func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error {
|
||||
ids = append(ids, apiID)
|
||||
}
|
||||
}
|
||||
return storeMailbox.api().MarkMessagesRead(ids)
|
||||
return storeMailbox.client().MarkMessagesRead(ids)
|
||||
}
|
||||
|
||||
// MarkMessagesUnread marks the message unread by calling an API.
|
||||
@ -128,7 +141,7 @@ func (storeMailbox *Mailbox) MarkMessagesUnread(apiIDs []string) error {
|
||||
"mailbox": storeMailbox.Name,
|
||||
}).Trace("Marking messages as unread")
|
||||
defer storeMailbox.pollNow()
|
||||
return storeMailbox.api().MarkMessagesUnread(apiIDs)
|
||||
return storeMailbox.client().MarkMessagesUnread(apiIDs)
|
||||
}
|
||||
|
||||
// MarkMessagesStarred adds the Starred label by calling an API.
|
||||
@ -141,7 +154,7 @@ func (storeMailbox *Mailbox) MarkMessagesStarred(apiIDs []string) error {
|
||||
"mailbox": storeMailbox.Name,
|
||||
}).Trace("Marking messages as starred")
|
||||
defer storeMailbox.pollNow()
|
||||
return storeMailbox.api().LabelMessages(apiIDs, pmapi.StarredLabel)
|
||||
return storeMailbox.client().LabelMessages(apiIDs, pmapi.StarredLabel)
|
||||
}
|
||||
|
||||
// MarkMessagesUnstarred removes the Starred label by calling an API.
|
||||
@ -154,7 +167,7 @@ func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error {
|
||||
"mailbox": storeMailbox.Name,
|
||||
}).Trace("Marking messages as unstarred")
|
||||
defer storeMailbox.pollNow()
|
||||
return storeMailbox.api().UnlabelMessages(apiIDs, pmapi.StarredLabel)
|
||||
return storeMailbox.client().UnlabelMessages(apiIDs, pmapi.StarredLabel)
|
||||
}
|
||||
|
||||
// DeleteMessages deletes messages.
|
||||
@ -197,17 +210,21 @@ func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
|
||||
}
|
||||
}
|
||||
if len(messageIDsToUnlabel) > 0 {
|
||||
if err := storeMailbox.api().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil {
|
||||
if err := storeMailbox.client().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil {
|
||||
log.WithError(err).Warning("Cannot unlabel before deleting")
|
||||
}
|
||||
}
|
||||
if len(messageIDsToDelete) > 0 {
|
||||
if err := storeMailbox.api().DeleteMessages(messageIDsToDelete); err != nil {
|
||||
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case pmapi.DraftLabel:
|
||||
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := storeMailbox.api().UnlabelMessages(apiIDs, storeMailbox.labelID); err != nil {
|
||||
if err := storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -249,6 +266,8 @@ func (storeMailbox *Mailbox) txSkipAndRemoveFromMailbox(tx *bolt.Tx, msg *pmapi.
|
||||
|
||||
// txCreateOrUpdateMessages will delete, create or update message from mailbox.
|
||||
func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi.Message) error { //nolint[funlen]
|
||||
shouldSendMailboxUpdate := false
|
||||
|
||||
// Buckets are not initialized right away because it's a heavy operation.
|
||||
// The best option is to get the same bucket only once and only when needed.
|
||||
var apiBucket, imapBucket *bolt.Bucket
|
||||
@ -264,7 +283,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
|
||||
|
||||
// Draft bodies can change and bodies are not re-fetched by IMAP clients.
|
||||
// Every change has to be a new message; we need to delete the old one and always recreate it.
|
||||
if storeMailbox.labelID == pmapi.DraftLabel {
|
||||
if msg.Type == pmapi.MessageTypeDraft {
|
||||
if err := storeMailbox.txDeleteMessage(tx, msg.ID); err != nil {
|
||||
return errors.Wrap(err, "cannot delete old draft")
|
||||
}
|
||||
@ -317,9 +336,16 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
|
||||
seqNum,
|
||||
msg,
|
||||
)
|
||||
shouldSendMailboxUpdate = true
|
||||
}
|
||||
|
||||
return storeMailbox.txMailboxStatusUpdate(tx)
|
||||
if shouldSendMailboxUpdate {
|
||||
if err := storeMailbox.txMailboxStatusUpdate(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// txDeleteMessage deletes the message from the mailbox bucket.
|
||||
@ -353,15 +379,16 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
|
||||
storeMailbox.labelName,
|
||||
seqNum,
|
||||
)
|
||||
if err := storeMailbox.txMailboxStatusUpdate(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
// Outlook for Mac has problems with sending an EXISTS after deleting
|
||||
// messages, mostly after moving message to other folder. It causes
|
||||
// Outlook to rebuild the whole mailbox. [RFC-3501] says it's not
|
||||
// necessary to send an EXISTS response with the new value.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
|
||||
total, unread, err := storeMailbox.txGetCounts(tx)
|
||||
total, unread, unreadSeqNum, err := storeMailbox.txGetCounts(tx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get counts for mailbox status update")
|
||||
}
|
||||
@ -370,6 +397,7 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
|
||||
storeMailbox.labelName,
|
||||
total,
|
||||
unread,
|
||||
unreadSeqNum,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -28,7 +28,6 @@ import (
|
||||
// a specific mailbox with helper functions to get IMAP UID, sequence
|
||||
// numbers and similar.
|
||||
type Message struct {
|
||||
api PMAPIProvider
|
||||
msg *pmapi.Message
|
||||
|
||||
store *Store
|
||||
@ -37,7 +36,6 @@ type Message struct {
|
||||
|
||||
func newStoreMessage(storeMailbox *Mailbox, msg *pmapi.Message) *Message {
|
||||
return &Message{
|
||||
api: storeMailbox.store.api,
|
||||
msg: msg,
|
||||
store: storeMailbox.store,
|
||||
storeMailbox: storeMailbox,
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/internal/store (interfaces: PanicHandler,BridgeUser)
|
||||
// Source: github.com/ProtonMail/proton-bridge/internal/store (interfaces: PanicHandler,ClientManager,BridgeUser)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockPanicHandler is a mock of PanicHandler interface
|
||||
@ -44,6 +46,43 @@ func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
|
||||
}
|
||||
|
||||
// MockClientManager is a mock of ClientManager interface
|
||||
type MockClientManager struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockClientManagerMockRecorder
|
||||
}
|
||||
|
||||
// MockClientManagerMockRecorder is the mock recorder for MockClientManager
|
||||
type MockClientManagerMockRecorder struct {
|
||||
mock *MockClientManager
|
||||
}
|
||||
|
||||
// NewMockClientManager creates a new mock instance
|
||||
func NewMockClientManager(ctrl *gomock.Controller) *MockClientManager {
|
||||
mock := &MockClientManager{ctrl: ctrl}
|
||||
mock.recorder = &MockClientManagerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockClientManager) EXPECT() *MockClientManagerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetClient mocks base method
|
||||
func (m *MockClientManager) GetClient(arg0 string) pmapi.Client {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetClient", arg0)
|
||||
ret0, _ := ret[0].(pmapi.Client)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetClient indicates an expected call of GetClient
|
||||
func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
|
||||
}
|
||||
|
||||
// MockBridgeUser is a mock of BridgeUser interface
|
||||
type MockBridgeUser struct {
|
||||
ctrl *gomock.Controller
|
||||
|
||||
@ -5,9 +5,10 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockListener is a mock of Listener interface
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
imapBackend "github.com/emersion/go-imap/backend"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -92,7 +93,7 @@ type Store struct {
|
||||
panicHandler PanicHandler
|
||||
eventLoop *eventLoop
|
||||
user BridgeUser
|
||||
api PMAPIProvider
|
||||
clientManager ClientManager
|
||||
|
||||
log *logrus.Entry
|
||||
|
||||
@ -101,9 +102,10 @@ type Store struct {
|
||||
db *bolt.DB
|
||||
lock *sync.RWMutex
|
||||
addresses map[string]*Address
|
||||
imapUpdates chan interface{}
|
||||
imapUpdates chan imapBackend.Update
|
||||
|
||||
isSyncRunning bool
|
||||
syncCooldown cooldown
|
||||
addressMode addressMode
|
||||
}
|
||||
|
||||
@ -111,13 +113,13 @@ type Store struct {
|
||||
func New(
|
||||
panicHandler PanicHandler,
|
||||
user BridgeUser,
|
||||
api PMAPIProvider,
|
||||
clientManager ClientManager,
|
||||
events listener.Listener,
|
||||
path string,
|
||||
cache *Cache,
|
||||
) (store *Store, err error) {
|
||||
if user == nil || api == nil || events == nil || cache == nil {
|
||||
return nil, fmt.Errorf("missing parameters - user: %v, api: %v, events: %v, cache: %v", user, api, events, cache)
|
||||
if user == nil || clientManager == nil || events == nil || cache == nil {
|
||||
return nil, fmt.Errorf("missing parameters - user: %v, api: %v, events: %v, cache: %v", user, clientManager, events, cache)
|
||||
}
|
||||
|
||||
l := log.WithField("user", user.ID())
|
||||
@ -139,7 +141,7 @@ func New(
|
||||
|
||||
store = &Store{
|
||||
panicHandler: panicHandler,
|
||||
api: api,
|
||||
clientManager: clientManager,
|
||||
user: user,
|
||||
cache: cache,
|
||||
filePath: path,
|
||||
@ -148,6 +150,9 @@ func New(
|
||||
log: l,
|
||||
}
|
||||
|
||||
// Minimal increase is event pollInterval, doubles every failed retry up to 5 minutes.
|
||||
store.syncCooldown.setExponentialWait(pollInterval, 2, 5*time.Minute)
|
||||
|
||||
if err = store.init(firstInit); err != nil {
|
||||
l.WithError(err).Error("Could not initialise store, attempting to close")
|
||||
if storeCloseErr := store.Close(); storeCloseErr != nil {
|
||||
@ -158,7 +163,7 @@ func New(
|
||||
}
|
||||
|
||||
if user.IsConnected() {
|
||||
store.eventLoop = newEventLoop(cache, store, api, user, events)
|
||||
store.eventLoop = newEventLoop(cache, store, user, events)
|
||||
go func() {
|
||||
defer store.panicHandler.HandlePanic()
|
||||
store.eventLoop.start()
|
||||
@ -261,10 +266,14 @@ func (store *Store) init(firstInit bool) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *Store) client() pmapi.Client {
|
||||
return store.clientManager.GetClient(store.UserID())
|
||||
}
|
||||
|
||||
// initCounts initialises the counts for each label. It tries to use the API first to fetch the labels but if
|
||||
// the API is unavailable for whatever reason it tries to fetch the labels locally.
|
||||
func (store *Store) initCounts() (labels []*pmapi.Label, err error) {
|
||||
if labels, err = store.api.ListLabels(); err != nil {
|
||||
if labels, err = store.client().ListLabels(); err != nil {
|
||||
store.log.WithError(err).Warn("Could not list API labels. Trying with local labels.")
|
||||
if labels, err = store.getLabelsFromLocalStorage(); err != nil {
|
||||
store.log.WithError(err).Error("Cannot list local labels")
|
||||
|
||||
@ -24,9 +24,9 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
bridgemocks "github.com/ProtonMail/proton-bridge/internal/bridge/mocks"
|
||||
storeMocks "github.com/ProtonMail/proton-bridge/internal/store/mocks"
|
||||
storemocks "github.com/ProtonMail/proton-bridge/internal/store/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -44,10 +44,11 @@ type mocksForStore struct {
|
||||
tb testing.TB
|
||||
|
||||
ctrl *gomock.Controller
|
||||
events *storeMocks.MockListener
|
||||
api *bridgemocks.MockPMAPIProvider
|
||||
user *storeMocks.MockBridgeUser
|
||||
panicHandler *storeMocks.MockPanicHandler
|
||||
events *storemocks.MockListener
|
||||
user *storemocks.MockBridgeUser
|
||||
client *pmapimocks.MockClient
|
||||
clientManager *storemocks.MockClientManager
|
||||
panicHandler *storemocks.MockPanicHandler
|
||||
store *Store
|
||||
|
||||
tmpDir string
|
||||
@ -59,10 +60,11 @@ func initMocks(tb testing.TB) (*mocksForStore, func()) {
|
||||
mocks := &mocksForStore{
|
||||
tb: tb,
|
||||
ctrl: ctrl,
|
||||
events: storeMocks.NewMockListener(ctrl),
|
||||
api: bridgemocks.NewMockPMAPIProvider(ctrl),
|
||||
user: storeMocks.NewMockBridgeUser(ctrl),
|
||||
panicHandler: storeMocks.NewMockPanicHandler(ctrl),
|
||||
events: storemocks.NewMockListener(ctrl),
|
||||
user: storemocks.NewMockBridgeUser(ctrl),
|
||||
client: pmapimocks.NewMockClient(ctrl),
|
||||
clientManager: storemocks.NewMockClientManager(ctrl),
|
||||
panicHandler: storemocks.NewMockPanicHandler(ctrl),
|
||||
}
|
||||
|
||||
// Called during clean-up.
|
||||
@ -92,13 +94,15 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unpar
|
||||
mocks.user.EXPECT().IsConnected().Return(true)
|
||||
mocks.user.EXPECT().IsCombinedAddressMode().Return(combinedMode)
|
||||
|
||||
mocks.api.EXPECT().Addresses().Return(pmapi.AddressList{
|
||||
mocks.clientManager.EXPECT().GetClient("userID").AnyTimes().Return(mocks.client)
|
||||
|
||||
mocks.client.EXPECT().Addresses().Return(pmapi.AddressList{
|
||||
{ID: addrID1, Email: addr1, Type: pmapi.OriginalAddress, Receive: pmapi.CanReceive},
|
||||
{ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: pmapi.CanReceive},
|
||||
})
|
||||
mocks.api.EXPECT().ListLabels()
|
||||
mocks.api.EXPECT().CountMessages("")
|
||||
mocks.api.EXPECT().GetEvent(gomock.Any()).
|
||||
mocks.client.EXPECT().ListLabels()
|
||||
mocks.client.EXPECT().CountMessages("")
|
||||
mocks.client.EXPECT().GetEvent(gomock.Any()).
|
||||
Return(&pmapi.Event{
|
||||
EventID: "latestEventID",
|
||||
}, nil).AnyTimes()
|
||||
@ -106,7 +110,7 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unpar
|
||||
// We want to wait until first sync has finished.
|
||||
firstSyncWaiter := sync.WaitGroup{}
|
||||
firstSyncWaiter.Add(1)
|
||||
mocks.api.EXPECT().
|
||||
mocks.client.EXPECT().
|
||||
ListMessages(gomock.Any()).
|
||||
DoAndReturn(func(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
|
||||
firstSyncWaiter.Done()
|
||||
@ -117,7 +121,7 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unpar
|
||||
mocks.store, err = New(
|
||||
mocks.panicHandler,
|
||||
mocks.user,
|
||||
mocks.api,
|
||||
mocks.clientManager,
|
||||
mocks.events,
|
||||
filepath.Join(mocks.tmpDir, "mailbox-test.db"),
|
||||
mocks.cache,
|
||||
|
||||
@ -28,6 +28,17 @@ import (
|
||||
|
||||
// TestSync triggers a sync of the store.
|
||||
func (store *Store) TestSync() {
|
||||
store.lock.Lock()
|
||||
defer store.lock.Unlock()
|
||||
|
||||
// Sync can happen any time. Sync assigns sequence numbers and UIDs
|
||||
// in the order of fetching from the server. We expect in the test
|
||||
// that sequence numbers and UIDs are assigned in the same order as
|
||||
// written in scenario setup. With more than one sync that cannot
|
||||
// be guaranteed so once test calls this function, first it has to
|
||||
// delete previous any already synced sequence numbers and UIDs.
|
||||
_ = store.truncateMailboxesBucket()
|
||||
|
||||
store.triggerSync()
|
||||
}
|
||||
|
||||
@ -46,6 +57,11 @@ func (store *Store) TestGetEventLoop() *eventLoop { //nolint[golint]
|
||||
return store.eventLoop
|
||||
}
|
||||
|
||||
// TestGetLastEvent returns last event processed by the store's event loop.
|
||||
func (store *Store) TestGetLastEvent() *pmapi.Event {
|
||||
return store.eventLoop.currentEvent
|
||||
}
|
||||
|
||||
// TestGetStoreFilePath returns the filepath of the store's database file.
|
||||
func (store *Store) TestGetStoreFilePath() string {
|
||||
return store.filePath
|
||||
@ -53,6 +69,12 @@ func (store *Store) TestGetStoreFilePath() string {
|
||||
|
||||
// TestDumpDB will dump store database content.
|
||||
func (store *Store) TestDumpDB(tb assert.TestingT) {
|
||||
if store == nil || store.db == nil {
|
||||
fmt.Printf(">>>>>>>> NIL STORE / DB <<<<<\n\n")
|
||||
assert.Fail(tb, "store or database is nil")
|
||||
return
|
||||
}
|
||||
|
||||
dumpCounts := true
|
||||
fmt.Printf(">>>>>>>> DUMP %s <<<<<\n\n", store.db.Path())
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ type messageLister interface {
|
||||
ListMessages(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error)
|
||||
}
|
||||
|
||||
func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api messageLister, syncState *syncState) error {
|
||||
func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api func() messageLister, syncState *syncState) error {
|
||||
labelID := pmapi.AllMailLabel
|
||||
|
||||
// When the full sync starts (i.e. is not already in progress), we need to load
|
||||
@ -53,7 +53,7 @@ func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api message
|
||||
return errors.Wrap(err, "failed to load message IDs")
|
||||
}
|
||||
|
||||
if err := findIDRanges(labelID, api, syncState); err != nil {
|
||||
if err := findIDRanges(labelID, api(), syncState); err != nil {
|
||||
return errors.Wrap(err, "failed to load IDs ranges")
|
||||
}
|
||||
syncState.save()
|
||||
@ -71,7 +71,7 @@ func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api message
|
||||
defer panicHandler.HandlePanic()
|
||||
defer wg.Done()
|
||||
|
||||
err := syncBatch(labelID, store, api, syncState, idRange, &shouldStop)
|
||||
err := syncBatch(labelID, store, api(), syncState, idRange, &shouldStop)
|
||||
if err != nil {
|
||||
shouldStop = 1
|
||||
resultError = errors.Wrap(err, "failed to sync group")
|
||||
|
||||
@ -26,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSyncState_IDRanges(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{}
|
||||
store := newSyncer()
|
||||
syncState := newSyncState(store, 0, []*syncIDRange{}, []string{})
|
||||
|
||||
syncState.initIDRanges()
|
||||
@ -43,7 +43,7 @@ func TestSyncState_IDRanges(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyncState_IDRangesLoaded(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{}
|
||||
store := newSyncer()
|
||||
syncState := newSyncState(store, 0, []*syncIDRange{
|
||||
{StartID: "", StopID: "100"},
|
||||
{StartID: "100", StopID: ""},
|
||||
@ -57,9 +57,9 @@ func TestSyncState_IDRangesLoaded(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyncState_IDsToBeDeleted(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{
|
||||
allMessageIDs: generateIDs(1, 9),
|
||||
}
|
||||
store := newSyncer()
|
||||
store.allMessageIDs = generateIDs(1, 9)
|
||||
|
||||
syncState := newSyncState(store, 0, []*syncIDRange{}, []string{})
|
||||
|
||||
require.Nil(t, syncState.loadMessageIDsToBeDeleted())
|
||||
@ -74,9 +74,9 @@ func TestSyncState_IDsToBeDeleted(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyncState_IDsToBeDeletedLoaded(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{
|
||||
allMessageIDs: generateIDs(1, 9),
|
||||
}
|
||||
store := newSyncer()
|
||||
store.allMessageIDs = generateIDs(1, 9)
|
||||
|
||||
syncState := newSyncState(store, 0, []*syncIDRange{}, generateIDs(4, 9))
|
||||
|
||||
idsToBeDeleted := syncState.getIDsToBeDeleted()
|
||||
|
||||
@ -20,6 +20,7 @@ package store
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -79,16 +80,29 @@ func (m *mockLister) ListMessages(filter *pmapi.MessagesFilter) (msgs []*pmapi.M
|
||||
}
|
||||
|
||||
type mockStoreSynchronizer struct {
|
||||
locker sync.Locker
|
||||
allMessageIDs []string
|
||||
errCreateOrUpdateMessagesEvent error
|
||||
createdMessageIDsByBatch [][]string
|
||||
}
|
||||
|
||||
func newSyncer() *mockStoreSynchronizer {
|
||||
return &mockStoreSynchronizer{
|
||||
locker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockStoreSynchronizer) getAllMessageIDs() ([]string, error) {
|
||||
m.locker.Lock()
|
||||
defer m.locker.Unlock()
|
||||
|
||||
return m.allMessageIDs, nil
|
||||
}
|
||||
|
||||
func (m *mockStoreSynchronizer) createOrUpdateMessagesEvent(messages []*pmapi.Message) error {
|
||||
m.locker.Lock()
|
||||
defer m.locker.Unlock()
|
||||
|
||||
if m.errCreateOrUpdateMessagesEvent != nil {
|
||||
return m.errCreateOrUpdateMessagesEvent
|
||||
}
|
||||
@ -101,10 +115,15 @@ func (m *mockStoreSynchronizer) createOrUpdateMessagesEvent(messages []*pmapi.Me
|
||||
}
|
||||
|
||||
func (m *mockStoreSynchronizer) deleteMessagesEvent([]string) error {
|
||||
m.locker.Lock()
|
||||
defer m.locker.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockStoreSynchronizer) saveSyncState(finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) {
|
||||
m.locker.Lock()
|
||||
defer m.locker.Unlock()
|
||||
}
|
||||
|
||||
func newTestSyncState(store storeSynchronizer, splitIDs ...string) *syncState {
|
||||
@ -173,12 +192,12 @@ func TestSyncAllMail(t *testing.T) { //nolint[funlen]
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{
|
||||
allMessageIDs: generateIDs(1, numberOfMessages+10),
|
||||
}
|
||||
store := newSyncer()
|
||||
store.allMessageIDs = generateIDs(1, numberOfMessages+10)
|
||||
|
||||
syncState := newSyncState(store, 0, tc.idRanges, tc.idsToBeDeleted)
|
||||
|
||||
err := syncAllMail(m.panicHandler, store, api, syncState)
|
||||
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Check all messages were created or updated.
|
||||
@ -217,16 +236,16 @@ func TestSyncAllMail_FailedListing(t *testing.T) {
|
||||
|
||||
numberOfMessages := 10000
|
||||
|
||||
store := &mockStoreSynchronizer{
|
||||
allMessageIDs: generateIDs(1, numberOfMessages+10),
|
||||
}
|
||||
store := newSyncer()
|
||||
store.allMessageIDs = generateIDs(1, numberOfMessages+10)
|
||||
|
||||
api := &mockLister{
|
||||
err: errors.New("error"),
|
||||
messageIDs: generateIDs(1, numberOfMessages),
|
||||
}
|
||||
syncState := newTestSyncState(store)
|
||||
|
||||
err := syncAllMail(m.panicHandler, store, api, syncState)
|
||||
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState)
|
||||
require.EqualError(t, err, "failed to sync group: failed to list messages: error")
|
||||
}
|
||||
|
||||
@ -236,21 +255,21 @@ func TestSyncAllMail_FailedCreateOrUpdateMessage(t *testing.T) {
|
||||
|
||||
numberOfMessages := 10000
|
||||
|
||||
store := &mockStoreSynchronizer{
|
||||
errCreateOrUpdateMessagesEvent: errors.New("error"),
|
||||
allMessageIDs: generateIDs(1, numberOfMessages+10),
|
||||
}
|
||||
store := newSyncer()
|
||||
store.errCreateOrUpdateMessagesEvent = errors.New("error")
|
||||
store.allMessageIDs = generateIDs(1, numberOfMessages+10)
|
||||
|
||||
api := &mockLister{
|
||||
messageIDs: generateIDs(1, numberOfMessages),
|
||||
}
|
||||
syncState := newTestSyncState(store)
|
||||
|
||||
err := syncAllMail(m.panicHandler, store, api, syncState)
|
||||
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState)
|
||||
require.EqualError(t, err, "failed to sync group: failed to create or update messages: error")
|
||||
}
|
||||
|
||||
func TestFindIDRanges(t *testing.T) { //nolint[funlen]
|
||||
store := &mockStoreSynchronizer{}
|
||||
store := newSyncer()
|
||||
syncState := newTestSyncState(store)
|
||||
|
||||
tests := []struct {
|
||||
@ -343,7 +362,7 @@ func TestFindIDRanges(t *testing.T) { //nolint[funlen]
|
||||
}
|
||||
|
||||
func TestFindIDRanges_FailedListing(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{}
|
||||
store := newSyncer()
|
||||
api := &mockLister{
|
||||
err: errors.New("error"),
|
||||
}
|
||||
@ -466,7 +485,7 @@ func TestSyncBatch(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{}
|
||||
store := newSyncer()
|
||||
api := &mockLister{
|
||||
messageIDs: generateIDs(1, 1000),
|
||||
}
|
||||
@ -479,7 +498,7 @@ func TestSyncBatch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyncBatch_FailedListing(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{}
|
||||
store := newSyncer()
|
||||
api := &mockLister{
|
||||
err: errors.New("error"),
|
||||
messageIDs: generateIDs(1, 1000),
|
||||
@ -490,9 +509,8 @@ func TestSyncBatch_FailedListing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyncBatch_FailedCreateOrUpdateMessage(t *testing.T) {
|
||||
store := &mockStoreSynchronizer{
|
||||
errCreateOrUpdateMessagesEvent: errors.New("error"),
|
||||
}
|
||||
store := newSyncer()
|
||||
store.errCreateOrUpdateMessagesEvent = errors.New("error")
|
||||
api := &mockLister{
|
||||
messageIDs: generateIDs(1, 1000),
|
||||
}
|
||||
|
||||
@ -17,42 +17,14 @@
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
import "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
|
||||
type PanicHandler interface {
|
||||
HandlePanic()
|
||||
}
|
||||
|
||||
// PMAPIProvider is subset of pmapi.Client for use by the Store.
|
||||
type PMAPIProvider interface {
|
||||
CurrentUser() (*pmapi.User, error)
|
||||
Addresses() pmapi.AddressList
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
SendMessage(messageID string, req *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err 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
|
||||
type ClientManager interface {
|
||||
GetClient(userID string) pmapi.Client
|
||||
}
|
||||
|
||||
// BridgeUser is subset of bridge.User for use by the Store.
|
||||
|
||||
@ -24,7 +24,7 @@ func (store *Store) UserID() string {
|
||||
|
||||
// GetSpace returns used and total space in bytes.
|
||||
func (store *Store) GetSpace() (usedSpace, maxSpace uint, err error) {
|
||||
apiUser, err := store.api.CurrentUser()
|
||||
apiUser, err := store.client().CurrentUser()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@ -33,7 +33,7 @@ func (store *Store) GetSpace() (usedSpace, maxSpace uint, err error) {
|
||||
|
||||
// GetMaxUpload returns max size of attachment in bytes.
|
||||
func (store *Store) GetMaxUpload() (uint, error) {
|
||||
apiUser, err := store.api.CurrentUser()
|
||||
apiUser, err := store.client().CurrentUser()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@ -46,6 +46,8 @@ func (store *Store) RebuildMailboxes() (err error) {
|
||||
|
||||
log.WithField("user", store.UserID()).Trace("Truncating mailboxes")
|
||||
|
||||
store.addresses = nil
|
||||
|
||||
if err = store.truncateMailboxesBucket(); err != nil {
|
||||
log.WithError(err).Error("Could not truncate mailboxes bucket")
|
||||
return
|
||||
@ -127,7 +129,12 @@ func (store *Store) createOrDeleteAddressesEvent() (err error) {
|
||||
delete(store.addresses, addr.addressID)
|
||||
}
|
||||
|
||||
return err
|
||||
if err = store.truncateMailboxesBucket(); err != nil {
|
||||
log.WithError(err).Error("Could not truncate mailboxes bucket")
|
||||
return
|
||||
}
|
||||
|
||||
return store.initMailboxesBucket()
|
||||
}
|
||||
|
||||
// truncateAddressInfoBucket removes the address info bucket.
|
||||
@ -153,8 +160,6 @@ func (store *Store) truncateAddressInfoBucket() (err error) {
|
||||
func (store *Store) truncateMailboxesBucket() (err error) {
|
||||
log.Trace("Truncating mailboxes bucket")
|
||||
|
||||
store.addresses = nil
|
||||
|
||||
tx := func(tx *bolt.Tx) (err error) {
|
||||
mbs := tx.Bucket(mailboxesBucket)
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ func (store *Store) GetAddressInfo() (addrs []AddressInfo, err error) {
|
||||
}
|
||||
|
||||
// Store does not have address info yet, need to build it first from API.
|
||||
addressList := store.api.Addresses()
|
||||
addressList := store.client().Addresses()
|
||||
if addressList == nil {
|
||||
err = errors.New("addresses unavailable")
|
||||
store.log.WithError(err).Error("Could not get user addresses from API")
|
||||
|
||||
@ -55,7 +55,7 @@ func (store *Store) createMailbox(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := store.api.CreateLabel(&pmapi.Label{
|
||||
_, err := store.client().CreateLabel(&pmapi.Label{
|
||||
Name: name,
|
||||
Color: color,
|
||||
Exclusive: exclusive,
|
||||
@ -133,7 +133,7 @@ func (store *Store) leastUsedColor() string {
|
||||
func (store *Store) updateMailbox(labelID, newName, color string) error {
|
||||
defer store.eventLoop.pollNow()
|
||||
|
||||
_, err := store.api.UpdateLabel(&pmapi.Label{
|
||||
_, err := store.client().UpdateLabel(&pmapi.Label{
|
||||
ID: labelID,
|
||||
Name: newName,
|
||||
Color: color,
|
||||
@ -150,15 +150,15 @@ func (store *Store) deleteMailbox(labelID, addressID string) error {
|
||||
var err error
|
||||
switch labelID {
|
||||
case pmapi.SpamLabel:
|
||||
err = store.api.EmptyFolder(pmapi.SpamLabel, addressID)
|
||||
err = store.client().EmptyFolder(pmapi.SpamLabel, addressID)
|
||||
case pmapi.TrashLabel:
|
||||
err = store.api.EmptyFolder(pmapi.TrashLabel, addressID)
|
||||
err = store.client().EmptyFolder(pmapi.TrashLabel, addressID)
|
||||
default:
|
||||
err = fmt.Errorf("cannot empty mailbox %v", labelID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return store.api.DeleteLabel(labelID)
|
||||
return store.client().DeleteLabel(labelID)
|
||||
}
|
||||
|
||||
func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error {
|
||||
@ -173,7 +173,7 @@ func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
labels, err := store.api.ListLabels()
|
||||
labels, err := store.client().ListLabels()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ import (
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -37,7 +37,7 @@ import (
|
||||
// If `attachedPublicKey` is passed, it's added to attachments.
|
||||
// Both draft and attachments are encrypted with passed `kr` key.
|
||||
func (store *Store) CreateDraft(
|
||||
kr *pmcrypto.KeyRing,
|
||||
kr *crypto.KeyRing,
|
||||
message *pmapi.Message,
|
||||
attachmentReaders []io.Reader,
|
||||
attachedPublicKey,
|
||||
@ -54,7 +54,7 @@ func (store *Store) CreateDraft(
|
||||
message.Attachments = nil
|
||||
|
||||
draftAction := store.getDraftAction(message)
|
||||
draft, err := store.api.CreateDraft(message, parentID, draftAction)
|
||||
draft, err := store.client().CreateDraft(message, parentID, draftAction)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to create draft")
|
||||
}
|
||||
@ -92,7 +92,7 @@ func (store *Store) getDraftAction(message *pmapi.Message) int {
|
||||
return pmapi.DraftActionReply
|
||||
}
|
||||
|
||||
func (store *Store) createAttachment(kr *pmcrypto.KeyRing, attachment *pmapi.Attachment, attachmentBody []byte) (*pmapi.Attachment, error) {
|
||||
func (store *Store) createAttachment(kr *crypto.KeyRing, attachment *pmapi.Attachment, attachmentBody []byte) (*pmapi.Attachment, error) {
|
||||
r := bytes.NewReader(attachmentBody)
|
||||
sigReader, err := attachment.DetachedSign(kr, r)
|
||||
if err != nil {
|
||||
@ -105,7 +105,7 @@ func (store *Store) createAttachment(kr *pmcrypto.KeyRing, attachment *pmapi.Att
|
||||
return nil, errors.Wrap(err, "failed to encrypt attachment")
|
||||
}
|
||||
|
||||
createdAttachment, err := store.api.CreateAttachment(attachment, encReader, sigReader)
|
||||
createdAttachment, err := store.client().CreateAttachment(attachment, encReader, sigReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create attachment")
|
||||
}
|
||||
@ -116,7 +116,7 @@ func (store *Store) createAttachment(kr *pmcrypto.KeyRing, attachment *pmapi.Att
|
||||
// SendMessage sends the message.
|
||||
func (store *Store) SendMessage(messageID string, req *pmapi.SendMessageReq) error {
|
||||
defer store.eventLoop.pollNow()
|
||||
_, _, err := store.api.SendMessage(messageID, req)
|
||||
_, _, err := store.client().SendMessage(messageID, req)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -142,19 +142,6 @@ func (store *Store) getMessageFromDB(apiID string) (msg *pmapi.Message, err erro
|
||||
return
|
||||
}
|
||||
|
||||
// fetchMessage returns pmapi struct of message by API ID. If the requested
|
||||
// message is not in the database, it will try to fetch it from the server.
|
||||
// NOTE: Do not update the database here to prevent issues (extreme edge case).
|
||||
// The database will be updated by the event loop anyway.
|
||||
func (store *Store) fetchMessage(apiID string) (msg *pmapi.Message, err error) {
|
||||
if msg, err = store.api.GetMessage(apiID); err != nil {
|
||||
if err.Error() == "Message does not exist" {
|
||||
return nil, ErrNoSuchAPIID
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (store *Store) txGetMessage(tx *bolt.Tx, apiID string) (*pmapi.Message, error) {
|
||||
b := tx.Bucket(metadataBucket)
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ func TestCreateOrUpdateMessageMetadata(t *testing.T) {
|
||||
a.Equal(t, []*pmapi.Attachment(nil), msg.Attachments)
|
||||
a.Equal(t, int64(-1), msg.Size)
|
||||
a.Equal(t, "", msg.MIMEType)
|
||||
a.Equal(t, mail.Header(nil), msg.Header)
|
||||
a.Equal(t, make(mail.Header), msg.Header)
|
||||
|
||||
// Change the calculated data.
|
||||
wantSize := int64(42)
|
||||
|
||||
@ -34,7 +34,7 @@ const syncIDsToBeDeletedKey = "ids_to_be_deleted"
|
||||
|
||||
// updateCountsFromServer will download and set the counts.
|
||||
func (store *Store) updateCountsFromServer() error {
|
||||
counts, err := store.api.CountMessages("")
|
||||
counts, err := store.client().CountMessages("")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot update counts from server")
|
||||
}
|
||||
@ -75,7 +75,7 @@ func (store *Store) isSynced(countsOnAPI []*pmapi.MessagesCount) (bool, error) {
|
||||
)
|
||||
}
|
||||
|
||||
mboxTot, mboxUnread, err := mbox.GetCounts()
|
||||
mboxTot, mboxUnread, _, err := mbox.GetCounts()
|
||||
if err != nil {
|
||||
errW := errors.Wrap(err, "cannot count messages")
|
||||
store.log.
|
||||
@ -128,11 +128,19 @@ func (store *Store) triggerSync() {
|
||||
store.log.Debug("Store sync triggered")
|
||||
|
||||
store.lock.Lock()
|
||||
|
||||
if store.isSyncRunning {
|
||||
store.lock.Unlock()
|
||||
store.log.Info("Store sync is already ongoing")
|
||||
return
|
||||
}
|
||||
|
||||
if store.syncCooldown.isTooSoon() {
|
||||
store.lock.Unlock()
|
||||
store.log.Info("Skipping sync: store tries to resync too often")
|
||||
return
|
||||
}
|
||||
|
||||
store.isSyncRunning = true
|
||||
store.lock.Unlock()
|
||||
|
||||
@ -144,12 +152,14 @@ func (store *Store) triggerSync() {
|
||||
|
||||
store.log.WithField("isIncomplete", syncState.isIncomplete()).Info("Store sync started")
|
||||
|
||||
err := syncAllMail(store.panicHandler, store, store.api, syncState)
|
||||
err := syncAllMail(store.panicHandler, store, func() messageLister { return store.client() }, syncState)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Store sync failed")
|
||||
store.syncCooldown.increaseWaitTime()
|
||||
return
|
||||
}
|
||||
|
||||
store.syncCooldown.reset()
|
||||
syncState.setFinishTime()
|
||||
}()
|
||||
}
|
||||
|
||||
@ -27,16 +27,20 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const sep = "\x00"
|
||||
const (
|
||||
sep = "\x00"
|
||||
|
||||
itemLengthBridge = 9
|
||||
itemLengthImportExport = 6 // Old format for Import/Export.
|
||||
)
|
||||
|
||||
var (
|
||||
log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
|
||||
log = logrus.WithField("pkg", "credentials") //nolint[gochecknoglobals]
|
||||
|
||||
ErrWrongFormat = errors.New("backend/creds: malformed password")
|
||||
ErrWrongFormat = errors.New("malformed credentials")
|
||||
)
|
||||
|
||||
type Credentials struct {
|
||||
@ -86,7 +90,7 @@ func (s *Credentials) Unmarshal(secret string) error {
|
||||
}
|
||||
items := strings.Split(string(b), sep)
|
||||
|
||||
if len(items) != 9 {
|
||||
if len(items) != itemLengthBridge && len(items) != itemLengthImportExport {
|
||||
return ErrWrongFormat
|
||||
}
|
||||
|
||||
@ -94,6 +98,9 @@ func (s *Credentials) Unmarshal(secret string) error {
|
||||
s.Emails = items[1]
|
||||
s.APIToken = items[2]
|
||||
s.MailboxPassword = items[3]
|
||||
|
||||
switch len(items) {
|
||||
case itemLengthBridge:
|
||||
s.BridgePassword = items[4]
|
||||
s.Version = items[5]
|
||||
if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil {
|
||||
@ -105,6 +112,13 @@ func (s *Credentials) Unmarshal(secret string) error {
|
||||
if s.IsCombinedAddressMode = false; items[8] == "1" {
|
||||
s.IsCombinedAddressMode = true
|
||||
}
|
||||
|
||||
case itemLengthImportExport:
|
||||
s.Version = items[4]
|
||||
if _, err = fmt.Sscan(items[5], &s.Timestamp); err != nil {
|
||||
s.Timestamp = 0
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
67
internal/users/credentials/credentials_test.go
Normal file
67
internal/users/credentials/credentials_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
r "github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var wantCredentials = Credentials{
|
||||
UserID: "1",
|
||||
Name: "name",
|
||||
Emails: "email1;email2",
|
||||
APIToken: "token",
|
||||
MailboxPassword: "mailbox pass",
|
||||
BridgePassword: "bridge pass",
|
||||
Version: "k11",
|
||||
Timestamp: time.Now().Unix(),
|
||||
IsHidden: false,
|
||||
IsCombinedAddressMode: false,
|
||||
}
|
||||
|
||||
func TestUnmarshallBridge(t *testing.T) {
|
||||
encoded := wantCredentials.Marshal()
|
||||
haveCredentials := Credentials{UserID: "1"}
|
||||
r.NoError(t, haveCredentials.Unmarshal(encoded))
|
||||
r.Equal(t, wantCredentials, haveCredentials)
|
||||
}
|
||||
|
||||
func TestUnmarshallImportExport(t *testing.T) {
|
||||
items := []string{
|
||||
wantCredentials.Name,
|
||||
wantCredentials.Emails,
|
||||
wantCredentials.APIToken,
|
||||
wantCredentials.MailboxPassword,
|
||||
"k11",
|
||||
fmt.Sprint(wantCredentials.Timestamp),
|
||||
}
|
||||
|
||||
str := strings.Join(items, sep)
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(str))
|
||||
|
||||
haveCredentials := Credentials{UserID: "1"}
|
||||
haveCredentials.BridgePassword = wantCredentials.BridgePassword // This one is not used.
|
||||
r.NoError(t, haveCredentials.Unmarshal(encoded))
|
||||
r.Equal(t, wantCredentials, haveCredentials)
|
||||
}
|
||||
@ -18,7 +18,6 @@
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
@ -36,8 +35,8 @@ type Store struct {
|
||||
}
|
||||
|
||||
// NewStore creates a new encrypted credentials store.
|
||||
func NewStore() (*Store, error) {
|
||||
secrets, err := keychain.NewAccess("bridge")
|
||||
func NewStore(appName string) (*Store, error) {
|
||||
secrets, err := keychain.NewAccess(appName)
|
||||
return &Store{
|
||||
secrets: secrets,
|
||||
}, err
|
||||
@ -67,18 +66,9 @@ func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails [
|
||||
|
||||
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
|
||||
}
|
||||
if err == nil {
|
||||
log.Info("Updating credentials of existing user")
|
||||
creds.BridgePassword = currentCredentials.BridgePassword
|
||||
creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode
|
||||
creds.Timestamp = currentCredentials.Timestamp
|
||||
@ -125,6 +115,20 @@ func (s *Store) UpdateEmails(userID string, emails []string) error {
|
||||
return s.saveCredentials(credentials)
|
||||
}
|
||||
|
||||
func (s *Store) UpdatePassword(userID, password string) error {
|
||||
storeLocker.Lock()
|
||||
defer storeLocker.Unlock()
|
||||
|
||||
credentials, err := s.get(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credentials.MailboxPassword = password
|
||||
|
||||
return s.saveCredentials(credentials)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateToken(userID, apiToken string) error {
|
||||
storeLocker.Lock()
|
||||
defer storeLocker.Unlock()
|
||||
@ -225,40 +229,9 @@ 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)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./listener/listener.go
|
||||
|
||||
// Package bridge is a generated GoMock package.
|
||||
package bridge
|
||||
// Package users is a generated GoMock package.
|
||||
package users
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
433
internal/users/mocks/mocks.go
Normal file
433
internal/users/mocks/mocks.go
Normal file
@ -0,0 +1,433 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/internal/users (interfaces: Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
store "github.com/ProtonMail/proton-bridge/internal/store"
|
||||
credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockConfiger is a mock of Configer interface
|
||||
type MockConfiger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockConfigerMockRecorder
|
||||
}
|
||||
|
||||
// MockConfigerMockRecorder is the mock recorder for MockConfiger
|
||||
type MockConfigerMockRecorder struct {
|
||||
mock *MockConfiger
|
||||
}
|
||||
|
||||
// NewMockConfiger creates a new mock instance
|
||||
func NewMockConfiger(ctrl *gomock.Controller) *MockConfiger {
|
||||
mock := &MockConfiger{ctrl: ctrl}
|
||||
mock.recorder = &MockConfigerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockConfiger) EXPECT() *MockConfigerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ClearData mocks base method
|
||||
func (m *MockConfiger) ClearData() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ClearData")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ClearData indicates an expected call of ClearData
|
||||
func (mr *MockConfigerMockRecorder) ClearData() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearData", reflect.TypeOf((*MockConfiger)(nil).ClearData))
|
||||
}
|
||||
|
||||
// GetAPIConfig mocks base method
|
||||
func (m *MockConfiger) GetAPIConfig() *pmapi.ClientConfig {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAPIConfig")
|
||||
ret0, _ := ret[0].(*pmapi.ClientConfig)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetAPIConfig indicates an expected call of GetAPIConfig
|
||||
func (mr *MockConfigerMockRecorder) GetAPIConfig() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIConfig", reflect.TypeOf((*MockConfiger)(nil).GetAPIConfig))
|
||||
}
|
||||
|
||||
// GetVersion mocks base method
|
||||
func (m *MockConfiger) GetVersion() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetVersion")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetVersion indicates an expected call of GetVersion
|
||||
func (mr *MockConfigerMockRecorder) GetVersion() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersion", reflect.TypeOf((*MockConfiger)(nil).GetVersion))
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// MockClientManager is a mock of ClientManager interface
|
||||
type MockClientManager struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockClientManagerMockRecorder
|
||||
}
|
||||
|
||||
// MockClientManagerMockRecorder is the mock recorder for MockClientManager
|
||||
type MockClientManagerMockRecorder struct {
|
||||
mock *MockClientManager
|
||||
}
|
||||
|
||||
// NewMockClientManager creates a new mock instance
|
||||
func NewMockClientManager(ctrl *gomock.Controller) *MockClientManager {
|
||||
mock := &MockClientManager{ctrl: ctrl}
|
||||
mock.recorder = &MockClientManagerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockClientManager) EXPECT() *MockClientManagerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AllowProxy mocks base method
|
||||
func (m *MockClientManager) AllowProxy() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AllowProxy")
|
||||
}
|
||||
|
||||
// AllowProxy indicates an expected call of AllowProxy
|
||||
func (mr *MockClientManagerMockRecorder) AllowProxy() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowProxy", reflect.TypeOf((*MockClientManager)(nil).AllowProxy))
|
||||
}
|
||||
|
||||
// CheckConnection mocks base method
|
||||
func (m *MockClientManager) CheckConnection() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CheckConnection")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CheckConnection indicates an expected call of CheckConnection
|
||||
func (mr *MockClientManagerMockRecorder) CheckConnection() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckConnection", reflect.TypeOf((*MockClientManager)(nil).CheckConnection))
|
||||
}
|
||||
|
||||
// DisallowProxy mocks base method
|
||||
func (m *MockClientManager) DisallowProxy() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "DisallowProxy")
|
||||
}
|
||||
|
||||
// DisallowProxy indicates an expected call of DisallowProxy
|
||||
func (mr *MockClientManagerMockRecorder) DisallowProxy() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisallowProxy", reflect.TypeOf((*MockClientManager)(nil).DisallowProxy))
|
||||
}
|
||||
|
||||
// GetAnonymousClient mocks base method
|
||||
func (m *MockClientManager) GetAnonymousClient() pmapi.Client {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAnonymousClient")
|
||||
ret0, _ := ret[0].(pmapi.Client)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetAnonymousClient indicates an expected call of GetAnonymousClient
|
||||
func (mr *MockClientManagerMockRecorder) GetAnonymousClient() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnonymousClient", reflect.TypeOf((*MockClientManager)(nil).GetAnonymousClient))
|
||||
}
|
||||
|
||||
// GetAuthUpdateChannel mocks base method
|
||||
func (m *MockClientManager) GetAuthUpdateChannel() chan pmapi.ClientAuth {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAuthUpdateChannel")
|
||||
ret0, _ := ret[0].(chan pmapi.ClientAuth)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetAuthUpdateChannel indicates an expected call of GetAuthUpdateChannel
|
||||
func (mr *MockClientManagerMockRecorder) GetAuthUpdateChannel() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthUpdateChannel", reflect.TypeOf((*MockClientManager)(nil).GetAuthUpdateChannel))
|
||||
}
|
||||
|
||||
// GetClient mocks base method
|
||||
func (m *MockClientManager) GetClient(arg0 string) pmapi.Client {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetClient", arg0)
|
||||
ret0, _ := ret[0].(pmapi.Client)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetClient indicates an expected call of GetClient
|
||||
func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
|
||||
}
|
||||
|
||||
// SetUserAgent mocks base method
|
||||
func (m *MockClientManager) SetUserAgent(arg0, arg1, arg2 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetUserAgent", arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SetUserAgent indicates an expected call of SetUserAgent
|
||||
func (mr *MockClientManagerMockRecorder) SetUserAgent(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserAgent", reflect.TypeOf((*MockClientManager)(nil).SetUserAgent), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// UpdatePassword mocks base method
|
||||
func (m *MockCredentialsStorer) UpdatePassword(arg0, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdatePassword", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdatePassword indicates an expected call of UpdatePassword
|
||||
func (mr *MockCredentialsStorerMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdatePassword), 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)
|
||||
}
|
||||
|
||||
// MockStoreMaker is a mock of StoreMaker interface
|
||||
type MockStoreMaker struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockStoreMakerMockRecorder
|
||||
}
|
||||
|
||||
// MockStoreMakerMockRecorder is the mock recorder for MockStoreMaker
|
||||
type MockStoreMakerMockRecorder struct {
|
||||
mock *MockStoreMaker
|
||||
}
|
||||
|
||||
// NewMockStoreMaker creates a new mock instance
|
||||
func NewMockStoreMaker(ctrl *gomock.Controller) *MockStoreMaker {
|
||||
mock := &MockStoreMaker{ctrl: ctrl}
|
||||
mock.recorder = &MockStoreMakerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockStoreMaker) EXPECT() *MockStoreMakerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// New mocks base method
|
||||
func (m *MockStoreMaker) New(arg0 store.BridgeUser) (*store.Store, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "New", arg0)
|
||||
ret0, _ := ret[0].(*store.Store)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// New indicates an expected call of New
|
||||
func (mr *MockStoreMakerMockRecorder) New(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockStoreMaker)(nil).New), arg0)
|
||||
}
|
||||
|
||||
// Remove mocks base method
|
||||
func (m *MockStoreMaker) Remove(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Remove", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Remove indicates an expected call of Remove
|
||||
func (mr *MockStoreMakerMockRecorder) Remove(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockStoreMaker)(nil).Remove), arg0)
|
||||
}
|
||||
61
internal/users/types.go
Normal file
61
internal/users/types.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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 users
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type Configer interface {
|
||||
ClearData() error
|
||||
GetVersion() string
|
||||
GetAPIConfig() *pmapi.ClientConfig
|
||||
}
|
||||
|
||||
type PanicHandler interface {
|
||||
HandlePanic()
|
||||
}
|
||||
|
||||
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
|
||||
UpdatePassword(userID, password string) error
|
||||
UpdateToken(userID, apiToken string) error
|
||||
Logout(userID string) error
|
||||
Delete(userID string) error
|
||||
}
|
||||
|
||||
type ClientManager interface {
|
||||
GetClient(userID string) pmapi.Client
|
||||
GetAnonymousClient() pmapi.Client
|
||||
AllowProxy()
|
||||
DisallowProxy()
|
||||
GetAuthUpdateChannel() chan pmapi.ClientAuth
|
||||
CheckConnection() error
|
||||
SetUserAgent(clientName, clientVersion, os string)
|
||||
}
|
||||
|
||||
type StoreMaker interface {
|
||||
New(user store.BridgeUser) (*store.Store, error)
|
||||
Remove(userID string) error
|
||||
}
|
||||
@ -15,61 +15,54 @@
|
||||
// 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
|
||||
package users
|
||||
|
||||
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/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
imapBackend "github.com/emersion/go-imap/backend"
|
||||
"github.com/pkg/errors"
|
||||
logrus "github.com/sirupsen/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")
|
||||
// ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from the app.
|
||||
var ErrLoggedOutUser = errors.New("account is logged out, use the app 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
|
||||
clientManager ClientManager
|
||||
credStorer CredentialsStorer
|
||||
|
||||
imapUpdatesChannel chan interface{}
|
||||
imapUpdatesChannel chan imapBackend.Update
|
||||
|
||||
storeFactory StoreMaker
|
||||
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
|
||||
isAuthorized bool
|
||||
}
|
||||
|
||||
// newUser creates a new bridge user.
|
||||
// newUser creates a new user.
|
||||
func newUser(
|
||||
panicHandler PanicHandler,
|
||||
userID string,
|
||||
eventListener listener.Listener,
|
||||
credStorer CredentialsStorer,
|
||||
apiClient PMAPIProvider,
|
||||
storeCache *store.Cache,
|
||||
storeDir string,
|
||||
clientManager ClientManager,
|
||||
storeFactory StoreMaker,
|
||||
) (u *User, err error) {
|
||||
log := log.WithField("user", userID)
|
||||
log.Debug("Creating or loading user")
|
||||
@ -84,9 +77,8 @@ func newUser(
|
||||
panicHandler: panicHandler,
|
||||
listener: eventListener,
|
||||
credStorer: credStorer,
|
||||
apiClient: apiClient,
|
||||
storeCache: storeCache,
|
||||
storePath: getUserStorePath(storeDir, userID),
|
||||
clientManager: clientManager,
|
||||
storeFactory: storeFactory,
|
||||
userID: userID,
|
||||
creds: creds,
|
||||
}
|
||||
@ -94,19 +86,17 @@ func newUser(
|
||||
return
|
||||
}
|
||||
|
||||
// init initialises a bridge user. This includes reloading its credentials from the credentials store
|
||||
func (u *User) client() pmapi.Client {
|
||||
return u.clientManager.GetClient(u.userID)
|
||||
}
|
||||
|
||||
// init initialises a user. This includes reloading its credentials from the credentials store
|
||||
// (such as when logging out and back in, you need to reload the credentials because the new credentials will
|
||||
// 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()
|
||||
func (u *User) init(idleUpdates chan imapBackend.Update) (err error) {
|
||||
u.log.Info("Initialising user")
|
||||
|
||||
// Reload the user's credentials (if they log out and back in we need the new
|
||||
// version with the apitoken and mailbox password).
|
||||
@ -116,17 +106,8 @@ func (u *User) init(idleUpdates chan interface{}, apiClient PMAPIProvider) (err
|
||||
}
|
||||
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.
|
||||
// Note: we still allow users to set up accounts if the internet is off.
|
||||
if authErr := u.authorizeIfNecessary(false); authErr != nil {
|
||||
switch errors.Cause(authErr) {
|
||||
case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser:
|
||||
@ -147,7 +128,7 @@ func (u *User) init(idleUpdates chan interface{}, apiClient PMAPIProvider) (err
|
||||
}
|
||||
u.store = nil
|
||||
}
|
||||
store, err := store.New(u.panicHandler, u, u.apiClient, u.listener, u.storePath, u.storeCache)
|
||||
store, err := u.storeFactory.New(u)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create store")
|
||||
}
|
||||
@ -169,10 +150,10 @@ func (u *User) SetIMAPIdleUpdateChannel() {
|
||||
|
||||
// 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.
|
||||
// it tries to connect it.
|
||||
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() {
|
||||
if u.creds.IsConnected() && u.isAuthorized {
|
||||
// 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).
|
||||
@ -209,22 +190,14 @@ func (u *User) authorizeIfNecessary(emitEvent bool) (err error) {
|
||||
|
||||
// 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 {
|
||||
if u.client().IsUnlocked() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
|
||||
if err := u.client().Unlock([]byte(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
|
||||
}
|
||||
|
||||
@ -236,49 +209,28 @@ func (u *User) authorizeAndUnlock() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
auth, err := u.apiClient.AuthRefresh(u.creds.APIToken)
|
||||
if err != nil {
|
||||
if _, err := u.client().AuthRefresh(u.creds.APIToken); err != nil {
|
||||
return errors.Wrap(err, "failed to refresh API auth")
|
||||
}
|
||||
u.authChannel <- auth
|
||||
|
||||
if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
|
||||
if err := u.client().Unlock([]byte(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")
|
||||
func (u *User) updateAuthToken(auth *pmapi.Auth) {
|
||||
u.log.Debug("User received auth")
|
||||
|
||||
if err := u.credStorer.UpdateToken(u.userID, auth.GenToken()); err != nil {
|
||||
u.log.WithError(err).Error("Failed to update refresh token in credentials store")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
u.isAuthorized = true
|
||||
}
|
||||
|
||||
// clearStore removes the database.
|
||||
@ -291,7 +243,7 @@ func (u *User) clearStore() error {
|
||||
}
|
||||
} 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 {
|
||||
if err := u.storeFactory.Remove(u.userID); err != nil {
|
||||
return errors.Wrap(err, "failed to remove store manually")
|
||||
}
|
||||
}
|
||||
@ -311,17 +263,11 @@ func (u *User) closeStore() error {
|
||||
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
|
||||
func (u *User) GetTemporaryPMAPIClient() pmapi.Client {
|
||||
return u.client()
|
||||
}
|
||||
|
||||
// ID returns the user's userID.
|
||||
@ -434,7 +380,7 @@ func (u *User) GetBridgePassword() string {
|
||||
}
|
||||
|
||||
// CheckBridgeLogin checks whether the user is logged in and the bridge
|
||||
// password is correct.
|
||||
// IMAP/SMTP password is correct.
|
||||
func (u *User) CheckBridgeLogin(password string) error {
|
||||
if isApplicationOutdated {
|
||||
u.listener.Emit(events.UpgradeApplicationEvent, "")
|
||||
@ -462,20 +408,16 @@ func (u *User) UpdateUser() error {
|
||||
return errors.Wrap(err, "cannot update user")
|
||||
}
|
||||
|
||||
_, err := u.apiClient.UpdateUser()
|
||||
_, err := u.client().UpdateUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil {
|
||||
return err
|
||||
if err = u.client().ReloadKeys([]byte(u.creds.MailboxPassword)); err != nil {
|
||||
return errors.Wrap(err, "failed to reload keys")
|
||||
}
|
||||
|
||||
if err := u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
emails := u.apiClient.Addresses().ActiveEmails()
|
||||
emails := u.client().Addresses().ActiveEmails()
|
||||
if err := u.credStorer.UpdateEmails(u.userID, emails); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -519,16 +461,21 @@ func (u *User) SwitchAddressMode() (err error) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// the server) which emits LogoutEvent to notify other parts of the app.
|
||||
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)
|
||||
}
|
||||
|
||||
u.isAuthorized = false
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -544,22 +491,7 @@ func (u *User) Logout() (err error) {
|
||||
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
|
||||
}
|
||||
u.client().Logout()
|
||||
|
||||
if err = u.credStorer.Logout(u.userID); err != nil {
|
||||
u.log.WithError(err).Warn("Could not log user out from credentials store")
|
||||
@ -575,6 +507,7 @@ func (u *User) Logout() (err error) {
|
||||
u.closeEventLoop()
|
||||
|
||||
u.closeAllConnections()
|
||||
|
||||
runtime.GC()
|
||||
|
||||
return err
|
||||
@ -582,7 +515,7 @@ func (u *User) Logout() (err error) {
|
||||
|
||||
func (u *User) refreshFromCredentials() {
|
||||
if credentials, err := u.credStorer.Get(u.userID); err != nil {
|
||||
log.Error("Cannot update credentials: ", err)
|
||||
log.WithError(err).Error("Cannot refresh user credentials")
|
||||
} else {
|
||||
u.creds = credentials
|
||||
}
|
||||
@ -615,7 +548,3 @@ func (u *User) CloseConnection(address string) {
|
||||
func (u *User) GetStore() *store.Store {
|
||||
return u.store
|
||||
}
|
||||
|
||||
func (u *User) HasAPIAuth() bool {
|
||||
return u.hasAPIAuth
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
package users
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@ -34,16 +34,22 @@ func TestUpdateUser(t *testing.T) {
|
||||
user := testNewUser(m)
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().IsUnlocked().Return(false),
|
||||
m.pmapiClient.EXPECT().Unlock([]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.pmapiClient.EXPECT().UpdateUser().Return(nil, nil),
|
||||
m.pmapiClient.EXPECT().ReloadKeys([]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)
|
||||
m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
)
|
||||
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).MaxTimes(1),
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1),
|
||||
)
|
||||
|
||||
assert.NoError(t, user.UpdateUser())
|
||||
|
||||
@ -105,9 +111,12 @@ func TestLogoutUser(t *testing.T) {
|
||||
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)
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().Logout().Return(),
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
err := user.Logout()
|
||||
@ -124,10 +133,12 @@ func TestLogoutUserFailsLogout(t *testing.T) {
|
||||
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)
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().Logout().Return(),
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed")),
|
||||
m.credentialsStore.EXPECT().Delete("user").Return(nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
)
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
err := user.Logout()
|
||||
@ -135,15 +146,17 @@ func TestLogoutUserFailsLogout(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCheckBridgeLogin(t *testing.T) {
|
||||
func TestCheckBridgeLoginOK(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)
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().IsUnlocked().Return(false),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
|
||||
)
|
||||
|
||||
err := user.CheckBridgeLogin(testCredentials.BridgePassword)
|
||||
|
||||
@ -152,6 +165,28 @@ func TestCheckBridgeLogin(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCheckBridgeLoginTwiceOK(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
user := testNewUser(m)
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().IsUnlocked().Return(false),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
|
||||
m.pmapiClient.EXPECT().IsUnlocked().Return(true),
|
||||
)
|
||||
|
||||
err := user.CheckBridgeLogin(testCredentials.BridgePassword)
|
||||
waitForEvents()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = user.CheckBridgeLogin(testCredentials.BridgePassword)
|
||||
waitForEvents()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCheckBridgeLoginUpgradeApplication(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
@ -162,11 +197,12 @@ func TestCheckBridgeLoginUpgradeApplication(t *testing.T) {
|
||||
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
|
||||
|
||||
isApplicationOutdated = true
|
||||
|
||||
err := user.CheckBridgeLogin("any-pass")
|
||||
waitForEvents()
|
||||
isApplicationOutdated = false
|
||||
|
||||
assert.Equal(t, pmapi.ErrUpgradeApplication, err)
|
||||
|
||||
isApplicationOutdated = false
|
||||
}
|
||||
|
||||
func TestCheckBridgeLoginLoggedOut(t *testing.T) {
|
||||
@ -174,22 +210,27 @@ func TestCheckBridgeLoginLoggedOut(t *testing.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)
|
||||
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
|
||||
assert.NoError(t, err)
|
||||
|
||||
m.clientManager.EXPECT().GetClient(gomock.Any()).Return(m.pmapiClient).MinTimes(1)
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized")),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||
)
|
||||
|
||||
err = user.init(nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||
|
||||
err := user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword)
|
||||
err = user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword)
|
||||
waitForEvents()
|
||||
|
||||
assert.Equal(t, "bridge account is logged out, use bridge to login again", err.Error())
|
||||
assert.Equal(t, ErrLoggedOutUser, err)
|
||||
}
|
||||
|
||||
func TestCheckBridgeLoginBadPassword(t *testing.T) {
|
||||
@ -199,8 +240,10 @@ func TestCheckBridgeLoginBadPassword(t *testing.T) {
|
||||
user := testNewUser(m)
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
|
||||
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().IsUnlocked().Return(false),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
|
||||
)
|
||||
|
||||
err := user.CheckBridgeLogin("wrong!")
|
||||
waitForEvents()
|
||||
151
internal/users/user_new_test.go
Normal file
151
internal/users/user_new_test.go
Normal file
@ -0,0 +1,151 @@
|
||||
// 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 users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"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.clientManager, m.storeMaker)
|
||||
a.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewUserAppOutdated(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrUpgradeApplication),
|
||||
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, ""),
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrUpgradeApplication),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||
)
|
||||
|
||||
checkNewUserHasCredentials(testCredentials, m)
|
||||
}
|
||||
|
||||
func TestNewUserNoInternetConnection(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrAPINotReachable),
|
||||
m.eventListener.EXPECT().Emit(events.InternetOffEvent, ""),
|
||||
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrAPINotReachable),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(nil, pmapi.ErrAPINotReachable).AnyTimes(),
|
||||
)
|
||||
|
||||
checkNewUserHasCredentials(testCredentials, m)
|
||||
}
|
||||
|
||||
func TestNewUserAuthRefreshFails(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token")),
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil),
|
||||
|
||||
m.pmapiClient.EXPECT().Logout(),
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
)
|
||||
|
||||
checkNewUserHasCredentials(testCredentialsDisconnected, m)
|
||||
}
|
||||
|
||||
func TestNewUserUnlockFails(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
|
||||
|
||||
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(errors.New("bad password")),
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil),
|
||||
m.pmapiClient.EXPECT().Logout(),
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
)
|
||||
|
||||
checkNewUserHasCredentials(testCredentialsDisconnected, m)
|
||||
}
|
||||
|
||||
func TestNewUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
mockConnectedUser(m)
|
||||
mockEventLoopNoAction(m)
|
||||
|
||||
checkNewUserHasCredentials(testCredentials, m)
|
||||
}
|
||||
|
||||
func checkNewUserHasCredentials(creds *credentials.Credentials, m mocks) {
|
||||
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
|
||||
defer cleanUpUserData(user)
|
||||
|
||||
_ = user.init(nil)
|
||||
|
||||
waitForEvents()
|
||||
|
||||
a.Equal(m.t, creds, user.creds)
|
||||
}
|
||||
|
||||
func _TestUserEventRefreshUpdatesAddresses(t *testing.T) { // nolint[funlen]
|
||||
a.Fail(t, "not implemented")
|
||||
}
|
||||
101
internal/users/user_test.go
Normal file
101
internal/users/user_test.go
Normal 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/>.
|
||||
|
||||
package users
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// testNewUser sets up a new, authorised user.
|
||||
func testNewUser(m mocks) *User {
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
mockConnectedUser(m)
|
||||
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).MaxTimes(1),
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).MaxTimes(1),
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1),
|
||||
)
|
||||
|
||||
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
|
||||
assert.NoError(m.t, err)
|
||||
|
||||
err = user.init(nil)
|
||||
assert.NoError(m.t, err)
|
||||
|
||||
mockAuthUpdate(user, "reftok", m)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func testNewUserForLogout(m mocks) *User {
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
mockConnectedUser(m)
|
||||
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).MaxTimes(1),
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).MaxTimes(1),
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1),
|
||||
)
|
||||
|
||||
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
|
||||
assert.NoError(m.t, err)
|
||||
|
||||
err = user.init(nil)
|
||||
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())
|
||||
}
|
||||
495
internal/users/users.go
Normal file
495
internal/users/users.go
Normal file
@ -0,0 +1,495 @@
|
||||
// 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 users provides core business logic providing API over credentials store and PM API.
|
||||
package users
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
imapBackend "github.com/emersion/go-imap/backend"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "users") //nolint[gochecknoglobals]
|
||||
isApplicationOutdated = false //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
// Users is a struct handling users.
|
||||
type Users struct {
|
||||
config Configer
|
||||
panicHandler PanicHandler
|
||||
events listener.Listener
|
||||
clientManager ClientManager
|
||||
credStorer CredentialsStorer
|
||||
storeFactory StoreMaker
|
||||
|
||||
// users is a list of accounts that have been added to the app.
|
||||
// They are stored sorted in the credentials store in the order
|
||||
// that they were added to the app 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 imapBackend.Update
|
||||
|
||||
lock sync.RWMutex
|
||||
|
||||
// stopAll can be closed to stop all goroutines from looping (watchAppOutdated, watchAPIAuths, heartbeat etc).
|
||||
stopAll chan struct{}
|
||||
}
|
||||
|
||||
func New(
|
||||
config Configer,
|
||||
panicHandler PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
clientManager ClientManager,
|
||||
credStorer CredentialsStorer,
|
||||
storeFactory StoreMaker,
|
||||
) *Users {
|
||||
log.Trace("Creating new users")
|
||||
|
||||
u := &Users{
|
||||
config: config,
|
||||
panicHandler: panicHandler,
|
||||
events: eventListener,
|
||||
clientManager: clientManager,
|
||||
credStorer: credStorer,
|
||||
storeFactory: storeFactory,
|
||||
idleUpdates: make(chan imapBackend.Update),
|
||||
lock: sync.RWMutex{},
|
||||
stopAll: make(chan struct{}),
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
u.watchAppOutdated()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
u.watchAPIAuths()
|
||||
}()
|
||||
|
||||
if u.credStorer == nil {
|
||||
log.Error("No credentials store is available")
|
||||
} else if err := u.loadUsersFromCredentialsStore(); err != nil {
|
||||
log.WithError(err).Error("Could not load all users from credentials store")
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *Users) loadUsersFromCredentialsStore() (err error) {
|
||||
u.lock.Lock()
|
||||
defer u.lock.Unlock()
|
||||
|
||||
userIDs, err := u.credStorer.List()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, userID := range userIDs {
|
||||
l := log.WithField("user", userID)
|
||||
|
||||
user, newUserErr := newUser(u.panicHandler, userID, u.events, u.credStorer, u.clientManager, u.storeFactory)
|
||||
if newUserErr != nil {
|
||||
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
u.users = append(u.users, user)
|
||||
|
||||
if initUserErr := user.init(u.idleUpdates); initUserErr != nil {
|
||||
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *Users) watchAppOutdated() {
|
||||
ch := make(chan string)
|
||||
|
||||
u.events.Add(events.UpgradeApplicationEvent, ch)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
isApplicationOutdated = true
|
||||
u.closeAllConnections()
|
||||
|
||||
case <-u.stopAll:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// watchAPIAuths receives auths from the client manager and sends them to the appropriate user.
|
||||
func (u *Users) watchAPIAuths() {
|
||||
for {
|
||||
select {
|
||||
case auth := <-u.clientManager.GetAuthUpdateChannel():
|
||||
log.Debug("Users received auth from ClientManager")
|
||||
|
||||
user, ok := u.hasUser(auth.UserID)
|
||||
if !ok {
|
||||
log.WithField("userID", auth.UserID).Info("User not available for auth update")
|
||||
continue
|
||||
}
|
||||
|
||||
if auth.Auth != nil {
|
||||
user.updateAuthToken(auth.Auth)
|
||||
} else if err := user.logout(); err != nil {
|
||||
log.WithError(err).
|
||||
WithField("userID", auth.UserID).
|
||||
Error("User logout failed while watching API auths")
|
||||
}
|
||||
|
||||
case <-u.stopAll:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Users) closeAllConnections() {
|
||||
for _, user := range u.users {
|
||||
user.closeAllConnections()
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates a user by username/password, returning an authorised client and an auth object.
|
||||
// The authorisation scope may not yet be full if the user has 2FA enabled.
|
||||
func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
|
||||
u.crashBandicoot(username)
|
||||
|
||||
// We need to use anonymous client because we don't yet have userID and so can't save auth tokens yet.
|
||||
authClient = u.clientManager.GetAnonymousClient()
|
||||
|
||||
authInfo, err := authClient.AuthInfo(username)
|
||||
if err != nil {
|
||||
log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
|
||||
return
|
||||
}
|
||||
|
||||
if auth, err = authClient.Auth(username, password, authInfo); err != nil {
|
||||
log.WithField("username", username).WithError(err).Error("Could not get auth for user")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FinishLogin finishes the login procedure and adds the user into the credentials store.
|
||||
func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassphrase string) (user *User, err error) { //nolint[funlen]
|
||||
defer func() {
|
||||
if err == pmapi.ErrUpgradeApplication {
|
||||
u.events.Emit(events.UpgradeApplicationEvent, "")
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("Login not finished; removing auth session")
|
||||
if delAuthErr := authClient.DeleteAuth(); delAuthErr != nil {
|
||||
log.WithError(delAuthErr).Error("Failed to clear login session after unlock")
|
||||
}
|
||||
}
|
||||
// The anonymous client will be removed from list and authentication will not be deleted.
|
||||
authClient.Logout()
|
||||
}()
|
||||
|
||||
apiUser, hashedPassphrase, err := getAPIUser(authClient, mbPassphrase)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to get API user")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Got API user")
|
||||
|
||||
var ok bool
|
||||
if user, ok = u.hasUser(apiUser.ID); ok {
|
||||
if err = u.connectExistingUser(user, auth, hashedPassphrase); err != nil {
|
||||
log.WithError(err).Error("Failed to connect existing user")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err = u.addNewUser(apiUser, auth, hashedPassphrase); err != nil {
|
||||
log.WithError(err).Error("Failed to add new user")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
u.events.Emit(events.UserRefreshEvent, apiUser.ID)
|
||||
|
||||
return u.GetUser(apiUser.ID)
|
||||
}
|
||||
|
||||
// connectExistingUser connects an existing user.
|
||||
func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassphrase string) (err error) {
|
||||
if user.IsConnected() {
|
||||
return errors.New("user is already connected")
|
||||
}
|
||||
|
||||
log.Info("Connecting existing user")
|
||||
|
||||
// Update the user's password in the cred store in case they changed it.
|
||||
if err = u.credStorer.UpdatePassword(user.ID(), hashedPassphrase); err != nil {
|
||||
return errors.Wrap(err, "failed to update password of user in credentials store")
|
||||
}
|
||||
|
||||
client := u.clientManager.GetClient(user.ID())
|
||||
|
||||
if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
|
||||
return errors.Wrap(err, "failed to refresh auth token of new client")
|
||||
}
|
||||
|
||||
if err = u.credStorer.UpdateToken(user.ID(), auth.GenToken()); err != nil {
|
||||
return errors.Wrap(err, "failed to update token of user in credentials store")
|
||||
}
|
||||
|
||||
if err = user.init(u.idleUpdates); err != nil {
|
||||
return errors.Wrap(err, "failed to initialise user")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// addNewUser adds a new user.
|
||||
func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassphrase string) (err error) {
|
||||
u.lock.Lock()
|
||||
defer u.lock.Unlock()
|
||||
|
||||
client := u.clientManager.GetClient(apiUser.ID)
|
||||
|
||||
if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
|
||||
return errors.Wrap(err, "failed to refresh token in new client")
|
||||
}
|
||||
|
||||
if apiUser, err = client.CurrentUser(); err != nil {
|
||||
return errors.Wrap(err, "failed to update API user")
|
||||
}
|
||||
|
||||
activeEmails := client.Addresses().ActiveEmails()
|
||||
|
||||
if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), hashedPassphrase, activeEmails); err != nil {
|
||||
return errors.Wrap(err, "failed to add user to credentials store")
|
||||
}
|
||||
|
||||
user, err := newUser(u.panicHandler, apiUser.ID, u.events, u.credStorer, u.clientManager, u.storeFactory)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create user")
|
||||
}
|
||||
|
||||
// The user needs to be part of the users list in order for it to receive an auth during initialisation.
|
||||
u.users = append(u.users, user)
|
||||
|
||||
if err = user.init(u.idleUpdates); err != nil {
|
||||
u.users = u.users[:len(u.users)-1]
|
||||
return errors.Wrap(err, "failed to initialise user")
|
||||
}
|
||||
|
||||
u.SendMetric(metrics.New(metrics.Setup, metrics.NewUser, metrics.NoLabel))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func getAPIUser(client pmapi.Client, mbPassphrase string) (user *pmapi.User, hashedPassphrase string, err error) {
|
||||
salt, err := client.AuthSalt()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Could not get salt")
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassphrase, err = pmapi.HashMailboxPassword(mbPassphrase, salt)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Could not hash mailbox password")
|
||||
return
|
||||
}
|
||||
|
||||
// We unlock the user's PGP key here to detect if the user's mailbox password is wrong.
|
||||
if err = client.Unlock([]byte(hashedPassphrase)); err != nil {
|
||||
log.WithError(err).Error("Wrong mailbox password")
|
||||
return
|
||||
}
|
||||
|
||||
if user, err = client.CurrentUser(); err != nil {
|
||||
log.WithError(err).Error("Could not load user data")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUsers returns all added users into keychain (even logged out users).
|
||||
func (u *Users) GetUsers() []*User {
|
||||
u.lock.RLock()
|
||||
defer u.lock.RUnlock()
|
||||
|
||||
return u.users
|
||||
}
|
||||
|
||||
// GetUser returns a user by `query` which is compared to users' ID, username or any attached e-mail address.
|
||||
func (u *Users) GetUser(query string) (*User, error) {
|
||||
u.crashBandicoot(query)
|
||||
|
||||
u.lock.RLock()
|
||||
defer u.lock.RUnlock()
|
||||
|
||||
for _, user := range u.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 (u *Users) ClearData() error {
|
||||
var result *multierror.Error
|
||||
for _, user := range u.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 := u.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 (u *Users) DeleteUser(userID string, clearStore bool) error {
|
||||
u.lock.Lock()
|
||||
defer u.lock.Unlock()
|
||||
|
||||
log := log.WithField("user", userID)
|
||||
|
||||
for idx, user := range u.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 := u.credStorer.Delete(userID); err != nil {
|
||||
log.WithError(err).Error("Cannot remove user")
|
||||
return err
|
||||
}
|
||||
u.users = append(u.users[:idx], u.users[idx+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("user " + userID + " not found")
|
||||
}
|
||||
|
||||
// SendMetric sends a metric. We don't want to return any errors, only log them.
|
||||
func (u *Users) SendMetric(m metrics.Metric) {
|
||||
c := u.clientManager.GetAnonymousClient()
|
||||
defer c.Logout()
|
||||
|
||||
cat, act, lab := m.Get()
|
||||
if err := c.SendSimpleMetric(string(cat), string(act), string(lab)); err != nil {
|
||||
log.Error("Sending metric failed: ", err)
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"cat": cat,
|
||||
"act": act,
|
||||
"lab": lab,
|
||||
}).Debug("Metric successfully sent")
|
||||
}
|
||||
|
||||
// GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
|
||||
func (u *Users) GetIMAPUpdatesChannel() chan imapBackend.Update {
|
||||
if u.idleUpdates == nil {
|
||||
log.Warn("IMAP updates channel is nil")
|
||||
}
|
||||
|
||||
return u.idleUpdates
|
||||
}
|
||||
|
||||
// AllowProxy instructs the app to use DoH to access an API proxy if necessary.
|
||||
// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
|
||||
func (u *Users) AllowProxy() {
|
||||
u.clientManager.AllowProxy()
|
||||
}
|
||||
|
||||
// DisallowProxy instructs the app to not use DoH to access an API proxy if necessary.
|
||||
// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
|
||||
func (u *Users) DisallowProxy() {
|
||||
u.clientManager.DisallowProxy()
|
||||
}
|
||||
|
||||
// CheckConnection returns whether there is an internet connection.
|
||||
// This should use the connection manager when it is eventually implemented.
|
||||
func (u *Users) CheckConnection() error {
|
||||
return u.clientManager.CheckConnection()
|
||||
}
|
||||
|
||||
// StopWatchers stops all goroutines.
|
||||
func (u *Users) StopWatchers() {
|
||||
close(u.stopAll)
|
||||
}
|
||||
|
||||
// hasUser returns whether the struct currently has a user with ID `id`.
|
||||
func (u *Users) hasUser(id string) (user *User, ok bool) {
|
||||
for _, u := range u.users {
|
||||
if u.ID() == id {
|
||||
user, ok = u, true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// "Easter egg" for testing purposes.
|
||||
func (u *Users) crashBandicoot(username string) {
|
||||
if username == "crash@bandicoot" {
|
||||
panic("Your wish is my command… I crash!")
|
||||
}
|
||||
}
|
||||
143
internal/users/users_actions_test.go
Normal file
143
internal/users/users_actions_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
// 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 users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNoUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
checkUsersGetUser(t, m, "nouser", -1, "user nouser not found")
|
||||
}
|
||||
|
||||
func TestGetUserByID(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
checkUsersGetUser(t, m, "user", 0, "")
|
||||
checkUsersGetUser(t, m, "users", 1, "")
|
||||
}
|
||||
|
||||
func TestGetUserByName(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
checkUsersGetUser(t, m, "username", 0, "")
|
||||
checkUsersGetUser(t, m, "usersname", 1, "")
|
||||
}
|
||||
|
||||
func TestGetUserByEmail(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
checkUsersGetUser(t, m, "user@pm.me", 0, "")
|
||||
checkUsersGetUser(t, m, "users@pm.me", 1, "")
|
||||
checkUsersGetUser(t, m, "anotheruser@pm.me", 1, "")
|
||||
checkUsersGetUser(t, m, "alsouser@pm.me", 1, "")
|
||||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
users := testNewUsersWithUsers(t, m)
|
||||
defer cleanUpUsersData(users)
|
||||
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().Logout().Return(),
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
m.credentialsStore.EXPECT().Delete("user").Return(nil),
|
||||
)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
err := users.DeleteUser("user", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users.users))
|
||||
}
|
||||
|
||||
// Even when logout fails, delete is done.
|
||||
func TestDeleteUserWithFailingLogout(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
users := testNewUsersWithUsers(t, m)
|
||||
defer cleanUpUsersData(users)
|
||||
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().Logout().Return(),
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed")),
|
||||
m.credentialsStore.EXPECT().Delete("user").Return(nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("no such user")),
|
||||
m.credentialsStore.EXPECT().Delete("user").Return(nil),
|
||||
)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
err := users.DeleteUser("user", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users.users))
|
||||
}
|
||||
|
||||
func checkUsersGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
|
||||
users := testNewUsersWithUsers(t, m)
|
||||
defer cleanUpUsersData(users)
|
||||
|
||||
user, err := users.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 = users.users[index]
|
||||
}
|
||||
|
||||
assert.Equal(m.t, expectedUser, user)
|
||||
}
|
||||
241
internal/users/users_login_test.go
Normal file
241
internal/users/users_login_test.go
Normal file
@ -0,0 +1,241 @@
|
||||
// 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 users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUsersFinishLoginBadMailboxPassword(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
err := errors.New("bad password")
|
||||
gomock.InOrder(
|
||||
// Init users with no user from keychain.
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil),
|
||||
|
||||
// Set up mocks for FinishLogin.
|
||||
m.pmapiClient.EXPECT().AuthSalt().Return("", nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(err),
|
||||
m.pmapiClient.EXPECT().DeleteAuth(),
|
||||
m.pmapiClient.EXPECT().Logout(),
|
||||
)
|
||||
|
||||
checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
|
||||
}
|
||||
|
||||
func TestUsersFinishLoginUpgradeApplication(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
err := errors.New("Cannot logout when upgrade needed")
|
||||
gomock.InOrder(
|
||||
// Init users with no user from keychain.
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil),
|
||||
|
||||
// Set up mocks for FinishLogin.
|
||||
m.pmapiClient.EXPECT().AuthSalt().Return("", nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(pmapi.ErrUpgradeApplication),
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, ""),
|
||||
m.pmapiClient.EXPECT().DeleteAuth().Return(err),
|
||||
m.pmapiClient.EXPECT().Logout(),
|
||||
)
|
||||
|
||||
checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
|
||||
}
|
||||
|
||||
func refreshWithToken(token string) *pmapi.Auth {
|
||||
return &pmapi.Auth{
|
||||
RefreshToken: token,
|
||||
}
|
||||
}
|
||||
|
||||
func credentialsWithToken(token string) *credentials.Credentials {
|
||||
tmp := &credentials.Credentials{}
|
||||
*tmp = *testCredentials
|
||||
tmp.APIToken = token
|
||||
return tmp
|
||||
}
|
||||
|
||||
func TestUsersFinishLoginNewUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
// Basically every call client has get client manager
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
gomock.InOrder(
|
||||
// users.New() finds no users in keychain.
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil),
|
||||
|
||||
// getAPIUser() loads user info from API (e.g. userID).
|
||||
m.pmapiClient.EXPECT().AuthSalt().Return("", nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil),
|
||||
|
||||
// addNewUser()
|
||||
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil),
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil),
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
|
||||
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email}),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil),
|
||||
|
||||
// user.init() in addNewUser
|
||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
|
||||
|
||||
// store.New() in user.init
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
|
||||
|
||||
// Emit event for new user and send metrics.
|
||||
m.clientManager.EXPECT().GetAnonymousClient().Return(m.pmapiClient),
|
||||
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.NewUser), string(metrics.NoLabel)),
|
||||
m.pmapiClient.EXPECT().Logout(),
|
||||
|
||||
// Reload account list in GUI.
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
|
||||
|
||||
// defer logout anonymous
|
||||
m.pmapiClient.EXPECT().Logout(),
|
||||
)
|
||||
|
||||
mockEventLoopNoAction(m)
|
||||
|
||||
user := checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
|
||||
|
||||
mockAuthUpdate(user, "afterCredentials", m)
|
||||
}
|
||||
|
||||
func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
loggedOutCreds := *testCredentials
|
||||
loggedOutCreds.APIToken = ""
|
||||
loggedOutCreds.MailboxPassword = ""
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
gomock.InOrder(
|
||||
// users.New() finds one existing user in keychain.
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil),
|
||||
|
||||
// newUser()
|
||||
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil),
|
||||
|
||||
// user.init()
|
||||
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil),
|
||||
|
||||
// store.New() in user.init
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrInvalidToken),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||
|
||||
// getAPIUser() loads user info from API (e.g. userID).
|
||||
m.pmapiClient.EXPECT().AuthSalt().Return("", nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil),
|
||||
|
||||
// connectExistingUser()
|
||||
m.credentialsStore.EXPECT().UpdatePassword("user", testCredentials.MailboxPassword).Return(nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil),
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":afterLogin").Return(nil),
|
||||
|
||||
// user.init() in connectExistingUser
|
||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
|
||||
|
||||
// store.New() in user.init
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
|
||||
|
||||
// Reload account list in GUI.
|
||||
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
|
||||
|
||||
// defer logout anonymous
|
||||
m.pmapiClient.EXPECT().Logout(),
|
||||
)
|
||||
|
||||
mockEventLoopNoAction(m)
|
||||
|
||||
user := checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
|
||||
|
||||
mockAuthUpdate(user, "afterCredentials", m)
|
||||
}
|
||||
|
||||
func TestUsersFinishLoginConnectedUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
||||
|
||||
mockConnectedUser(m)
|
||||
mockEventLoopNoAction(m)
|
||||
|
||||
users := testNewUsers(t, m)
|
||||
defer cleanUpUsersData(users)
|
||||
|
||||
// Then, try to log in again...
|
||||
gomock.InOrder(
|
||||
m.pmapiClient.EXPECT().AuthSalt().Return("", nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
|
||||
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil),
|
||||
m.pmapiClient.EXPECT().DeleteAuth(),
|
||||
m.pmapiClient.EXPECT().Logout(),
|
||||
)
|
||||
|
||||
_, err := users.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
|
||||
assert.Equal(t, "user is already connected", err.Error())
|
||||
}
|
||||
|
||||
func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) *User {
|
||||
users := testNewUsers(t, m)
|
||||
defer cleanUpUsersData(users)
|
||||
|
||||
user, err := users.FinishLogin(m.pmapiClient, auth, mailboxPassword)
|
||||
|
||||
waitForEvents()
|
||||
|
||||
assert.Equal(t, expectedErr, err)
|
||||
|
||||
if expectedUserID != "" {
|
||||
assert.Equal(t, expectedUserID, user.ID())
|
||||
assert.Equal(t, 1, len(users.users))
|
||||
assert.Equal(t, expectedUserID, users.users[0].ID())
|
||||
} else {
|
||||
assert.Equal(t, (*User)(nil), user)
|
||||
assert.Equal(t, 0, len(users.users))
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
179
internal/users/users_new_test.go
Normal file
179
internal/users/users_new_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
// 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 users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewUsersNoKeychain(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain"))
|
||||
|
||||
checkUsersNew(t, m, []*credentials.Credentials{})
|
||||
}
|
||||
|
||||
func TestNewUsersWithoutUsersInCredentialsStore(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
||||
|
||||
checkUsersNew(t, m, []*credentials.Credentials{})
|
||||
}
|
||||
|
||||
func TestNewUsersWithDisconnectedUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
// Basically every call client has get client manager.
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil),
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized")),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||
)
|
||||
|
||||
checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
||||
}
|
||||
|
||||
func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
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()
|
||||
m.credentialsStore.EXPECT().Logout("user").Return(nil)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
|
||||
checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
|
||||
}
|
||||
|
||||
func mockConnectedUser(m mocks) {
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
|
||||
|
||||
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
|
||||
|
||||
// Set up mocks for store initialisation for the authorized user.
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
|
||||
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
|
||||
)
|
||||
}
|
||||
|
||||
// mockAuthUpdate simulates users calling UpdateAuthToken on the given user.
|
||||
// This would normally be done by users when it receives an auth from the ClientManager,
|
||||
// but as we don't have a full users instance here, we do this manually.
|
||||
func mockAuthUpdate(user *User, token string, m mocks) {
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().UpdateToken("user", ":"+token).Return(nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(token), nil),
|
||||
)
|
||||
|
||||
user.updateAuthToken(refreshWithToken(token))
|
||||
|
||||
waitForEvents()
|
||||
}
|
||||
|
||||
func TestNewUsersWithConnectedUser(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
|
||||
|
||||
mockConnectedUser(m)
|
||||
mockEventLoopNoAction(m)
|
||||
|
||||
checkUsersNew(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 users.
|
||||
func TestNewUsersWithUsers(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"userDisconnected", "user"}, nil)
|
||||
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().Get("userDisconnected").Return(testCredentialsDisconnected, nil),
|
||||
m.credentialsStore.EXPECT().Get("userDisconnected").Return(testCredentialsDisconnected, nil),
|
||||
// Set up mocks for store initialisation for the unauth user.
|
||||
m.clientManager.EXPECT().GetClient("userDisconnected").Return(m.pmapiClient),
|
||||
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized")),
|
||||
m.clientManager.EXPECT().GetClient("userDisconnected").Return(m.pmapiClient),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(nil),
|
||||
)
|
||||
|
||||
mockConnectedUser(m)
|
||||
|
||||
mockEventLoopNoAction(m)
|
||||
|
||||
checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
|
||||
}
|
||||
|
||||
func TestNewUsersFirstStart(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
|
||||
|
||||
testNewUsers(t, m)
|
||||
}
|
||||
|
||||
func checkUsersNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
|
||||
users := testNewUsers(t, m)
|
||||
defer cleanUpUsersData(users)
|
||||
|
||||
assert.Equal(m.t, len(expectedCredentials), len(users.GetUsers()))
|
||||
|
||||
credentials := []*credentials.Credentials{}
|
||||
for _, user := range users.users {
|
||||
credentials = append(credentials, user.creds)
|
||||
}
|
||||
|
||||
assert.Equal(m.t, expectedCredentials, credentials)
|
||||
}
|
||||
@ -15,27 +15,31 @@
|
||||
// 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
|
||||
package users
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"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/internal/users/credentials"
|
||||
usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("VERBOSITY") == "fatal" {
|
||||
logrus.SetLevel(logrus.FatalLevel)
|
||||
}
|
||||
if os.Getenv("VERBOSITY") == "trace" {
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
}
|
||||
@ -45,11 +49,9 @@ func TestMain(m *testing.M) {
|
||||
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]
|
||||
@ -125,18 +127,39 @@ type mocks struct {
|
||||
t *testing.T
|
||||
|
||||
ctrl *gomock.Controller
|
||||
config *bridgemocks.MockConfiger
|
||||
PanicHandler *bridgemocks.MockPanicHandler
|
||||
prefProvider *bridgemocks.MockPreferenceProvider
|
||||
pmapiClient *bridgemocks.MockPMAPIProvider
|
||||
credentialsStore *bridgemocks.MockCredentialsStorer
|
||||
config *usersmocks.MockConfiger
|
||||
PanicHandler *usersmocks.MockPanicHandler
|
||||
clientManager *usersmocks.MockClientManager
|
||||
credentialsStore *usersmocks.MockCredentialsStorer
|
||||
storeMaker *usersmocks.MockStoreMaker
|
||||
eventListener *MockListener
|
||||
|
||||
pmapiClient *pmapimocks.MockClient
|
||||
|
||||
storeCache *store.Cache
|
||||
}
|
||||
|
||||
type fullStackReporter struct {
|
||||
T testing.TB
|
||||
}
|
||||
|
||||
func (fr *fullStackReporter) Errorf(format string, args ...interface{}) {
|
||||
fmt.Printf("err: "+format+"\n", args...)
|
||||
fr.T.Fail()
|
||||
}
|
||||
func (fr *fullStackReporter) Fatalf(format string, args ...interface{}) {
|
||||
debug.PrintStack()
|
||||
fmt.Printf("fail: "+format+"\n", args...)
|
||||
fr.T.FailNow()
|
||||
}
|
||||
|
||||
func initMocks(t *testing.T) mocks {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
var mockCtrl *gomock.Controller
|
||||
if os.Getenv("VERBOSITY") == "trace" {
|
||||
mockCtrl = gomock.NewController(&fullStackReporter{t})
|
||||
} else {
|
||||
mockCtrl = gomock.NewController(t)
|
||||
}
|
||||
|
||||
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
|
||||
require.NoError(t, err, "could not get temporary file for store cache")
|
||||
@ -145,84 +168,84 @@ func initMocks(t *testing.T) 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),
|
||||
config: usersmocks.NewMockConfiger(mockCtrl),
|
||||
PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl),
|
||||
clientManager: usersmocks.NewMockClientManager(mockCtrl),
|
||||
credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl),
|
||||
storeMaker: usersmocks.NewMockStoreMaker(mockCtrl),
|
||||
eventListener: NewMockListener(mockCtrl),
|
||||
|
||||
pmapiClient: pmapimocks.NewMockClient(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()
|
||||
|
||||
// Set up store factory.
|
||||
m.storeMaker.EXPECT().New(gomock.Any()).DoAndReturn(func(user store.BridgeUser) (*store.Store, error) {
|
||||
dbFile, err := ioutil.TempFile("", "bridge-store-db-*.db")
|
||||
require.NoError(t, err, "could not get temporary file for store db")
|
||||
return store.New(m.PanicHandler, user, m.clientManager, m.eventListener, dbFile.Name(), m.storeCache)
|
||||
}).AnyTimes()
|
||||
m.storeMaker.EXPECT().Remove(gomock.Any()).AnyTimes()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge {
|
||||
func testNewUsersWithUsers(t *testing.T, m mocks) *Users {
|
||||
// Events are asynchronous
|
||||
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).Times(2)
|
||||
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).Times(2)
|
||||
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).Times(2)
|
||||
|
||||
gomock.InOrder(
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil),
|
||||
|
||||
// Init for user.
|
||||
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)
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte("pass")).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}),
|
||||
|
||||
// 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().Get("users").Return(testCredentialsSplit, nil),
|
||||
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil),
|
||||
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
|
||||
m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil),
|
||||
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
|
||||
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
|
||||
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses),
|
||||
)
|
||||
|
||||
m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil)
|
||||
users := testNewUsers(t, m)
|
||||
|
||||
return testNewBridge(t, m)
|
||||
user, _ := users.GetUser("user")
|
||||
mockAuthUpdate(user, "reftok", m)
|
||||
|
||||
user, _ = users.GetUser("user")
|
||||
mockAuthUpdate(user, "reftok", m)
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
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()
|
||||
func testNewUsers(t *testing.T, m mocks) *Users { //nolint[unparam]
|
||||
m.config.EXPECT().GetVersion().Return("ver").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
|
||||
}
|
||||
m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth))
|
||||
|
||||
bridge := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, "ver", pmapiClientFactory, m.credentialsStore)
|
||||
users := New(m.config, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore, m.storeMaker)
|
||||
|
||||
waitForEvents()
|
||||
|
||||
return bridge
|
||||
return users
|
||||
}
|
||||
|
||||
func cleanUpBridgeUserData(b *Bridge) {
|
||||
func cleanUpUsersData(b *Users) {
|
||||
for _, user := range b.users {
|
||||
_ = user.clearStore()
|
||||
}
|
||||
@ -232,8 +255,11 @@ func TestClearData(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
bridge := testNewBridgeWithUsers(t, m)
|
||||
defer cleanUpBridgeUserData(bridge)
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
|
||||
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
|
||||
|
||||
users := testNewUsersWithUsers(t, m)
|
||||
defer cleanUpUsersData(users)
|
||||
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
|
||||
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me")
|
||||
@ -250,7 +276,18 @@ func TestClearData(t *testing.T) {
|
||||
|
||||
m.config.EXPECT().ClearData().Return(nil)
|
||||
|
||||
require.NoError(t, bridge.ClearData())
|
||||
require.NoError(t, users.ClearData())
|
||||
|
||||
waitForEvents()
|
||||
}
|
||||
|
||||
func mockEventLoopNoAction(m mocks) {
|
||||
// Set up mocks for starting the store's event loop (in store.New).
|
||||
// The event loop runs in another goroutine so this might happen at any time.
|
||||
gomock.InOrder(
|
||||
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),
|
||||
)
|
||||
}
|
||||
23
internal/users/users_test_exports.go
Normal file
23
internal/users/users_test_exports.go
Normal 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 users
|
||||
|
||||
// IsAuthorized returns whether the user has received an Auth from the API yet.
|
||||
func (u *User) IsAuthorized() bool {
|
||||
return u.isAuthorized
|
||||
}
|
||||
@ -19,20 +19,16 @@ package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-appdir"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
log = GetLogEntry("config") //nolint[gochecknoglobals]
|
||||
log = logrus.WithField("pkg", "config") //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
type appDirProvider interface {
|
||||
@ -48,7 +44,6 @@ type Config struct {
|
||||
cacheVersion string
|
||||
appDirs appDirProvider
|
||||
appDirsVersion appDirProvider
|
||||
apiConfig *pmapi.ClientConfig
|
||||
}
|
||||
|
||||
// New returns fully initialized config struct.
|
||||
@ -70,17 +65,6 @@ func newConfig(appName, version, revision, cacheVersion string, appDirs, appDirs
|
||||
cacheVersion: cacheVersion,
|
||||
appDirs: appDirs,
|
||||
appDirsVersion: appDirsVersion,
|
||||
apiConfig: &pmapi.ClientConfig{
|
||||
AppVersion: strings.Title(appName) + "_" + version,
|
||||
ClientID: appName,
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
},
|
||||
// TokenManager should not be required, but PMAPI still doesn't handle not-set cases everywhere.
|
||||
TokenManager: pmapi.NewTokenManager(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +110,7 @@ func (c *Config) ClearOldData() error {
|
||||
fileName := filepath.Base(filePath)
|
||||
return (fileName != c.cacheVersion &&
|
||||
!logFileRgx.MatchString(fileName) &&
|
||||
filePath != c.GetLogDir() &&
|
||||
filePath != c.GetTLSCertPath() &&
|
||||
filePath != c.GetTLSKeyPath() &&
|
||||
filePath != c.GetEventsPath() &&
|
||||
@ -188,6 +173,11 @@ func (c *Config) IsDevMode() bool {
|
||||
return os.Getenv("PROTONMAIL_ENV") == "dev"
|
||||
}
|
||||
|
||||
// GetVersion returns the version.
|
||||
func (c *Config) GetVersion() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
// GetLogDir returns folder for log files.
|
||||
func (c *Config) GetLogDir() string {
|
||||
return c.appDirs.UserLogs()
|
||||
@ -238,11 +228,6 @@ func (c *Config) GetPreferencesPath() string {
|
||||
return filepath.Join(c.appDirsVersion.UserCache(), "prefs.json")
|
||||
}
|
||||
|
||||
// GetAPIConfig returns config for ProtonMail API.
|
||||
func (c *Config) GetAPIConfig() *pmapi.ClientConfig {
|
||||
return c.apiConfig
|
||||
}
|
||||
|
||||
// GetDefaultAPIPort returns default Bridge local API port.
|
||||
func (c *Config) GetDefaultAPIPort() int {
|
||||
return 1042
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user