Compare commits

...

161 Commits

Author SHA1 Message Date
01a8c9e9d7 Adding GUI troubleshoot popup GODT-554 GODT-583 2020-08-06 08:12:37 +02:00
2c910378ce feat: detect bad certificate error 2020-08-06 07:34:36 +02:00
7baa4dc117 release notes 2020-07-29 07:07:50 +02:00
f073301481 fix: versioning 2020-07-17 13:04:11 +02:00
bf0945eaef fix: race condition in AuthRefresh that could cause user to be logged out 2020-07-16 10:19:50 +02:00
11e01ca163 chore: bump version to 1.3.0 2020-07-15 15:36:55 +02:00
a650a04a88 Bump bbolt dependency 2020-07-15 13:27:40 +02:00
bdc11c8358 ci: enable automated builds on all platforms 2020-07-14 14:57:25 +00:00
ed7a0dc9b3 fix: don't assume contact keys are armored 2020-07-14 16:43:06 +02:00
fc4e77604f fix: don't panic if not given tls connection in pin checker 2020-07-09 13:19:32 +02:00
abaeace4b3 chore: bump go-imap version to get select fix 2020-07-08 10:34:18 +02:00
457b524ba8 chore: bump go-imap to include delimiter fix 2020-07-07 11:10:37 +00:00
d89d627349 test: increase minimum listener event receive time 2020-07-06 16:11:53 +02:00
51ff880fd9 test: fix flaky test TestSyncAllMail 2020-07-03 14:43:44 +02:00
b25baa2524 test: set sent label properly 2020-07-03 07:45:16 +00:00
10e384f4df test: add tests for parsing mime message with bad 2231 filename 2020-07-03 09:19:18 +02:00
35ae2011b6 Merge branch master into devel 2020-07-02 10:16:08 +02:00
5348ae7d18 Changelog wording 2020-07-01 09:19:11 +02:00
2512d3647a chore: bump linter to v1.27.0 2020-07-01 07:06:55 +00:00
1e8cb35fcb test: add test for multiline 2020-06-30 16:33:29 +02:00
0b0991d682 fix: infinite loop when decoding invalid 2231 charset 2020-06-29 15:40:46 +02:00
813e99f399 Fix flaky integration tests 2020-06-26 09:51:56 +00:00
7301e5571c fix: return error if parsing header fails GODT-502 2020-06-26 11:35:07 +02:00
b6707749e5 chore: bump go-imap dependencies 2020-06-25 10:52:51 +02:00
7ec4309ae1 fix: correctly handle failure to unlock single key 2020-06-24 14:22:26 +02:00
ec224a962f fix: hang when reloading keys 2020-06-22 10:19:13 +02:00
012be60311 test: remove time checks 2020-06-17 15:30:41 +02:00
02804d067c fix: ensure doh connections are closed when it is disabled 2020-06-17 10:57:12 +02:00
9241a9bdbf feat: add reloadkeys method 2020-06-16 12:51:28 +02:00
f3e6af5571 feat: clear keys after unmarshaling 2020-06-16 10:23:21 +02:00
7a13b89274 test: reword scenario 2020-06-16 07:34:46 +00:00
5cb78b0a03 fix: review comments 2020-06-16 07:34:46 +00:00
c19bb0fa97 feat: migrate to gopenpgp v2 2020-06-16 07:34:46 +00:00
de16f6f2d1 Apply suggestion to internal/store/mailbox_message.go 2020-06-16 09:15:16 +02:00
7963b3c152 Apply suggestion to internal/store/mailbox_message.go 2020-06-16 07:05:35 +00:00
f82ab3189b Apply suggestion to internal/store/mailbox_message.go 2020-06-16 07:05:35 +00:00
49cc49b1e2 [GODT-354] Do not label/unlabel messsages from All Mail folder 2020-06-16 07:05:35 +00:00
9808c44714 fix: avoid listing credentials, prefer getting 2020-06-15 14:27:01 +02:00
c329711f9c fix: bad fish 2020-06-11 14:01:58 +02:00
928fa93765 fix: don't remove log dir on startup 2020-06-05 10:48:34 +02:00
45e99caa23 fix: handle double charset everywhere by using our ParseMediaType 2020-06-03 12:51:31 +00:00
80b2bfc2a5 fix: crash in message.combineParts when copying a nil slice 2020-06-03 12:41:51 +00:00
6070a3b7cc fix: crash if fail to find necessary html element 2020-06-03 14:05:20 +02:00
9e633400b0 feat: [GODT-360] detect charset embedded in html and xml 2020-06-02 09:44:50 +02:00
84d344cb0a chore: bump docker-credential-helpers version 2020-05-29 14:54:43 +02:00
e43033b42b feat: revert back to quoted-printable 2020-05-29 12:21:48 +00:00
e5d63edb62 test: add message.Parse tests 2020-05-29 12:21:48 +00:00
579e962980 check license 2020-05-29 14:01:10 +02:00
2919d1a3c0 fix: properly find parent id 2020-05-28 06:53:00 +00:00
1ba319bb69 Pimp up changelog 2020-05-28 06:18:29 +00:00
8cdebb6d05 Fix flaky store cooldown test 2020-05-27 15:20:38 +00:00
cc14b523cb fix: correct doh timeouts 2020-05-27 07:32:26 +00:00
ad877431de fix: check doh permission 2020-05-27 07:32:26 +00:00
40d8c458d2 Store factory to make store optional 2020-05-26 14:57:41 +00:00
3b0b1a457b docs: add locations of bridge files to readme 2020-05-26 08:02:15 +02:00
7ac4c9aecf fix: don't logout user if auth refresh fails because internet dropped 2020-05-25 15:21:20 +00:00
390182d247 do not complie windows twice, always pack to tgz 2020-05-25 13:46:50 +00:00
cb8a15a9fd fix: crash when removing account while messages are being returned 2020-05-25 08:29:42 +00:00
4d2baa6b85 Renamed bridge to general users and keep bridge only for bridge stuff 2020-05-25 09:02:34 +02:00
7724ca3996 release notes 2020-05-23 09:05:58 +00:00
4393d67bf2 GODT-396 reduce number of exists calls 2020-05-23 09:05:58 +00:00
d222b39793 Apply suggestion to test/features/imap/message/create.feature 2020-05-23 11:07:06 +02:00
6ae78217db Fix appending to Sent 2020-05-23 11:07:06 +02:00
b91c286332 fix gitlab dind 2020-05-23 10:55:57 +02:00
4e2ab9b389 Validate recipient emails in send before asking for their public keys 2020-05-21 07:26:34 +00:00
c6c6cfc7d7 Fix Changelog history 2020-05-21 09:27:46 +02:00
a78b1ca00f refactor: remove dead code 2020-05-20 11:33:22 +02:00
d718720b29 fix: custom message bad pgp using template 2020-05-20 10:59:00 +02:00
f64cb4b56d fix: wrong zip format 2020-05-19 16:19:51 +02:00
b2b43ac909 fix: missing ci package zip 2020-05-18 12:45:42 +00:00
f2b8d02cd2 fix: can't connect to docker 2020-05-18 14:19:19 +02:00
50ed40f205 release notes 2020-05-18 14:02:29 +02:00
3c92ff18ff feat: use zip for windows targetos 2020-05-18 12:35:27 +02:00
41f6cd3bcd feat: build windows in pipeline 2020-05-18 12:13:44 +02:00
66f23bef99 feat: cross compilation for windows 2020-05-18 10:28:06 +02:00
bbf1364e30 feat: tls report cache 2020-05-14 14:12:41 +02:00
6147c214c3 Better error message when request is canceled 2020-05-12 10:49:04 +00:00
f87ca36ffd refactor: tidy up DecodeCharset 2020-05-12 10:12:19 +00:00
37f4e46bdc feat: fallback to latin1 if charset not specified and not utf8 2020-05-12 10:12:19 +00:00
a7b9572e6b Fix reference parsing 2020-05-11 14:48:12 +00:00
4090c490b1 Apply suggestion to pkg/pmapi/messages.go 2020-05-11 14:48:12 +00:00
d33d7237bd Apply suggestion to pkg/pmapi/messages.go 2020-05-11 14:48:12 +00:00
9ed778f2b3 Apply suggestion to pkg/pmapi/messages.go 2020-05-11 14:48:12 +00:00
70fca64a36 Pop-out messageID format into constants 2020-05-11 14:48:12 +00:00
30425d5fcd Fix few typos 2020-05-11 14:48:12 +00:00
2639f7333e Simplify references parsing 2020-05-11 14:48:12 +00:00
4a8d07d54e Update auto-generated files 2020-05-07 19:14:30 +00:00
833fce8702 chore: bump linter 2020-05-07 16:24:10 +00:00
c1a57a2e12 Apply suggestion to internal/store/event_loop.go 2020-05-07 16:19:06 +02:00
cda3000a7a Apply suggestion to internal/store/store_test_exports.go 2020-05-07 16:19:06 +02:00
4b2977041a fix: missing messages after changing primary address 2020-05-07 16:19:06 +02:00
2d200f6f8c test: add test with changing address order 2020-05-07 16:19:06 +02:00
c61e8bdc71 Merge remote-tracking branch 'origin/master' into devel 2020-05-07 15:30:08 +02:00
7e8dc22837 Add build-files into .gitignore 2020-05-06 05:05:35 +00:00
e43bd231ed Final touches of go-imap v1 implementation 2020-05-05 11:47:47 +00:00
1998d92432 Updates() needs to return imapBackend.Update instead of interface with go-imap v1 2020-05-05 11:47:47 +00:00
313e803fdd Implement new SearchCriteria from latest go-imap 2020-05-05 11:47:47 +00:00
cabcb3ae2b Upgrade to latest go-imap-quota with fix for go-imap v1 2020-05-05 11:47:47 +00:00
e57a3c2a3a Notify about new mailbox 2020-05-05 11:47:47 +00:00
7a87a7ea2f fix: fixes after rebase 2020-05-05 11:47:47 +00:00
c939893131 Unseen is first sequence number of unseen message not count of messages 2020-05-05 11:47:47 +00:00
ea0f3115a3 usage of latest upstream go-imap 2020-05-05 11:47:47 +00:00
3d3b91b242 Merge commit comments 2020-05-05 11:20:39 +00:00
cd38c86b4b CI build mac 2020-05-05 11:20:39 +00:00
2379598078 fix: Build without GNU sed 2020-05-05 11:08:08 +00:00
984b28e8f9 User Agent do not contain bridge version, only client in format 2020-05-05 11:00:18 +00:00
1d49a484a8 test: fix typo 2020-05-04 12:31:51 +02:00
99aabf07b3 Apply suggestion to pkg/config/pmapi_prod.go 2020-05-04 07:53:55 +00:00
6e537db5ff Apply suggestion to pkg/pmapi/client.go 2020-05-04 07:53:55 +00:00
668fc7f039 feat: MinSpeed -> MinBytesPerSecond, check every 3 seconds 2020-05-04 07:53:55 +00:00
284a097d4f fix: lower min speed 2020-05-04 07:53:55 +00:00
e5944518ca chore: improve logging 2020-05-04 07:37:51 +00:00
df3a9ea19e test: add comment for why tests are disabled 2020-05-04 07:37:51 +00:00
2db1b113e0 fix: correct timeouts according to spec 2020-05-04 07:37:51 +00:00
68d2591c73 test: fix tls tests 2020-05-04 07:37:51 +00:00
e9735c6110 refactor: set app version when enabling remote tls issue reporting 2020-05-04 07:37:51 +00:00
0fd5ca3a24 feat: dialer refactor to support modular dialing/checking/proxying 2020-05-04 07:37:51 +00:00
8c2f88fe70 Apply suggestion to Changelog.md 2020-04-30 09:20:03 +00:00
23f492705b fix: better draft detection for parentID 2020-04-30 09:20:03 +00:00
44233e5bd3 fix 404 errors by using absolute urls 2020-04-30 09:12:26 +00:00
33770ce129 fix: crash in fakeapi if user is nil 2020-04-30 09:03:16 +00:00
faec347054 test: use the correct constants.Version in integration tests 2020-04-30 09:44:15 +02:00
9b68625522 Update BUILDS.md to list libsecret dev files
Fixes #11
2020-04-29 15:02:39 +02:00
d42deb2ad5 fix: variable name in readme 2020-04-28 12:39:05 +00:00
bb5227c1f4 fix: app version and variable location 2020-04-28 12:39:05 +00:00
0589f329e9 refactor: dedicated constants package, no explicit bridge version 2020-04-28 12:39:05 +00:00
522cadb8b1 refactor: dedicated constants package, no explicit bridge version 2020-04-28 12:39:05 +00:00
32ca7b3903 fix: envvar conflict on fedora 2020-04-28 12:39:05 +00:00
7d30459417 test: empty auth update channel in tests 2020-04-28 12:21:54 +00:00
8f15041d8f fix: race condition when updating user auth 2020-04-28 12:21:54 +00:00
51846efed5 Merge branch 'release/v1.2.7' into devel 2020-04-27 15:54:02 +02:00
a51841158c docs: add libglvnd as build deps 2020-04-22 10:16:22 +02:00
1457005f86 fix: address review comments 2020-04-21 13:29:26 +02:00
febdf98349 test: attempt less flaky tests 2020-04-21 08:36:39 +00:00
d4482994ec fix: missing and incorrect comments 2020-04-21 08:36:39 +00:00
244a18ac8c feat: update changelog 2020-04-21 08:36:39 +00:00
e027aa5fae test: use clientmanager to logout fakeapi 2020-04-21 08:36:39 +00:00
99635cd56d feat: max retries of 5 for client logout 2020-04-21 08:36:39 +00:00
e95aece6d3 refactor: don't pass client directly to store syncer 2020-04-21 08:36:39 +00:00
38f0425670 refactor: make sentry report its own package 2020-04-21 08:36:39 +00:00
4809d97cb1 feat: clientmanager has checkconnection 2020-04-21 08:36:39 +00:00
bfc4069df4 feat: remove user from bridge users list if init failed 2020-04-21 08:36:39 +00:00
3f32fd95e0 feat: refresh expired access tokens in one goroutine 2020-04-21 08:36:39 +00:00
40e96b9d1e feat: retry client auth delete while api is unreachable 2020-04-21 08:36:39 +00:00
80f4e1e346 Fixing unit tests for client manager.
* [x] pmapi: refresh auth uid won't change
* [x] bridge tests:
    * update mocks
    * delete auth when FinishLogin fails
    * check for mailbox password
    * add `gomock.InOrder` for better test control
* [x] fix linter issues except TODOs
* [x] make rootScheme unexported
* [x] store tests: update mocks
2020-04-21 08:36:39 +00:00
debd374d75 fix: don't delete uid of anonymous clients 2020-04-21 08:36:39 +00:00
ed8595fa5b test: some work on integration tests (fake) 2020-04-21 08:36:39 +00:00
fec5f2d3c3 test: fix most integration tests (live) 2020-04-21 08:36:39 +00:00
bafd4e714e refactor: remove unnecessary getters 2020-04-21 08:36:39 +00:00
d787d8b223 fix: use clientsLocker mutex 2020-04-21 08:36:39 +00:00
abca7284dd refactor: make getHost and getScheme private 2020-04-21 08:36:39 +00:00
db02eb694d refactor: no more pmapifactory 2020-04-21 08:36:39 +00:00
5bf4d9c6f5 refactor: prefer anonymous clients 2020-04-21 08:36:39 +00:00
b01be382fc refactor: GetBridgeAuthChannel --> GetAuthUpdateChannel 2020-04-21 08:36:38 +00:00
042c340881 feat: make store use ClientManager 2020-04-21 08:36:38 +00:00
f269be4291 refactor: make pmapi.Client the interface 2020-04-21 08:36:38 +00:00
6e38a65bd8 feat: improve login flow 2020-04-21 08:36:38 +00:00
941e09079c feat: implement token expiration watcher 2020-04-21 08:36:38 +00:00
ce29d4d74e feat: switch to proxy when need be 2020-04-21 08:36:38 +00:00
f239e8f3bf feat: central auth channel for clients 2020-04-21 08:36:38 +00:00
0a55fac29a feat: simple client manager 2020-04-21 08:36:38 +00:00
280 changed files with 9636 additions and 6307 deletions

15
.gitignore vendored
View File

@ -23,3 +23,18 @@ frontend/qml/ProtonUI/*.qmlc
frontend/qml/ProtonUI/fontawesome.ttf frontend/qml/ProtonUI/fontawesome.ttf
frontend/qml/ProtonUI/images frontend/qml/ProtonUI/images
frontend/qml/*.qmlc 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/

View File

@ -37,6 +37,8 @@ build-ci-image:
- ci/* - ci/*
services: services:
- docker:dind - docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
script: script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker info - docker info
@ -104,7 +106,54 @@ build-linux:
script: script:
- make build - make build
artifacts: 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: paths:
- bridge_*.tgz - bridge_*.tgz
expire_in: 2 week expire_in: 2 week

View File

@ -6,6 +6,7 @@
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/) * For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (linux, windows) or Xcode (macOS) * GCC (linux, windows) or Xcode (macOS)
* Windres (windows) * Windres (windows)
* libglvnd and libsecret development files (linux)
To enable the sending of crash reports using Sentry please set the To enable the sending of crash reports using Sentry please set the
`main.DSNSentry` value with the client key of your sentry project before build. `main.DSNSentry` value with the client key of your sentry project before build.

View File

@ -26,48 +26,48 @@ ProtonMail Bridge includes the following 3rd party software:
* [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing) * [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing)
* [Font Awesome 4.7.0](https://fontawesome.com/v4.7.0/) | Available under [multiple licenses](https://fontawesome.com/v4.7.0/license/) * [Font Awesome 4.7.0](https://fontawesome.com/v4.7.0/) | Available under [multiple licenses](https://fontawesome.com/v4.7.0/license/)
* [notificator](github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE) * [notificator](https://github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
* [ishell](github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE) * [ishell](https://github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
* [readline](github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE) * [readline](https://github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
* [singleinstance](github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE) * [singleinstance](https://github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
* [cascadia](github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE) * [cascadia](https://github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
* [gocertifi](github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE) * [gocertifi](https://github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE)
* [logex](github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE) * [logex](https://github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
* [test](github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE) * [test](https://github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
* [godog](github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE) * [godog](https://github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
* [wincred](github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE) * [wincred](https://github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
* [credential-helpers](github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE) * [credential-helpers](https://github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
* [imap](github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE) * [imap](https://github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
* [imap-appendlimit](github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE) * [imap-appendlimit](https://github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
* [imap-idle](github.com/emersion/go-imap-idle) | Available under [license](https://github.com/emersion/go-imap-idle/blob/master/LICENSE) * [imap-idle](https://github.com/emersion/go-imap-idle) | Available under [license](https://github.com/emersion/go-imap-idle/blob/master/LICENSE)
* [imap-quota](github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE) * [imap-quota](https://github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
* [imap-specialuse](github.com/emersion/go-imap-specialuse) | Available under [license](https://github.com/emersion/go-imap-specialuse/blob/master/LICENSE) * [imap-specialuse](https://github.com/emersion/go-imap-specialuse) | Available under [license](https://github.com/emersion/go-imap-specialuse/blob/master/LICENSE)
* [sasl](github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE) * [sasl](https://github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
* [smtp](github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE) * [smtp](https://github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
* [textwrapper](github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE) * [textwrapper](https://github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [vcard](github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE) * [vcard](https://github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [color](github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE) * [color](https://github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE.md)
* [shlex](github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE) * [shlex](https://github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/COPYING)
* [raven](github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE) * [raven](https://github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE)
* [resty](github.com/go-resty/resty/v2) | Available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE) * [resty](https://github.com/go-resty/resty) | Available under [license](https://github.com/go-resty/resty/blob/master/LICENSE)
* [mock](github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE) * [mock](https://github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE)
* [cmp](github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE) * [cmp](https://github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
* [gopherjs](github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE) * [gopherjs](https://github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE)
* [multierror](github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE) * [multierror](https://github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
* [bcrypt](github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE) * [bcrypt](https://github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
* [html2text](github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE) * [html2text](https://github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [enmime](github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE) * [enmime](https://github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE)
* [osext](github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE) * [osext](https://github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE)
* [keychain](github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE) * [keychain](https://github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [aurora](github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE) * [aurora](https://github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
* [dns](github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE) * [dns](https://github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [uuid](github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE) * [uuid](https://github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE)
* [jsondiff](github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE) * [jsondiff](https://github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
* [logrus](github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE) * [logrus](https://github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
* [golang](github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE) * [golang](https://github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE)
* [testify](github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE) * [testify](https://github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [uuid](github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE) * [uuid](https://github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE)
* [cli](github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE) * [cli](https://github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE)
* [BBolt](https://pkg.go.dev/go.etcd.io/bbolt/?tab=doc) | Available under [license](https://pkg.go.dev/go.etcd.io/bbolt?tab=licenses#LICENSE) * [BBolt](https://pkg.go.dev/go.etcd.io/bbolt/?tab=doc) | Available under [license](https://pkg.go.dev/go.etcd.io/bbolt?tab=licenses#LICENSE)
* [testify.v1](https://gopkg.in/stretchr/testify.v1) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE) * [testify.v1](https://gopkg.in/stretchr/testify.v1) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,21 @@
export GO111MODULE=on export GO111MODULE=on
# By default, the target OS is the same as the host OS,
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
GOOS:=$(shell go env GOOS) GOOS:=$(shell go env GOOS)
TARGET_OS?=${GOOS}
## Build ## Build
.PHONY: build build-nogui check-has-go .PHONY: build build-nogui check-has-go
VERSION?=1.2.7-git BRIDGE_VERSION?=$(shell git describe --abbrev=0 --tags)-git
REVISION:=$(shell git rev-parse --short=10 HEAD) REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z) BUILD_TIME:=$(shell date +%FT%T%z)
BUILD_TAGS?=pmapi_prod BUILD_TAGS?=pmapi_prod
BUILD_FLAGS:=-tags='${BUILD_TAGS}' BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui' BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
GO_LDFLAGS:=$(addprefix -X main.,Version=${VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME}) GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/pkg/constants.,Version=${BRIDGE_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
ifneq "${BUILD_LDFLAGS}" "" ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+= ${BUILD_LDFLAGS} GO_LDFLAGS+= ${BUILD_LDFLAGS}
endif endif
@ -23,25 +27,26 @@ DEPLOY_DIR:=cmd/Desktop-Bridge/deploy
ICO_FILES:= ICO_FILES:=
EXE:=$(shell basename ${CURDIR}) EXE:=$(shell basename ${CURDIR})
ifeq "${GOOS}" "windows" ifeq "${TARGET_OS}" "windows"
EXE+=.exe EXE:=${EXE}.exe
ICO_FILES:=logo.ico icon.rc icon_windows.syso ICO_FILES:=logo.ico icon.rc icon_windows.syso
endif endif
ifeq "${GOOS}" "darwin" ifeq "${TARGET_OS}" "darwin"
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
EXE:=${EXE}.app/Contents/MacOS/${EXE} EXE:=${EXE}.app/Contents/MacOS/${EXE}
endif endif
EXE_TARGET:=${DEPLOY_DIR}/${GOOS}/${EXE} EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
TGZ_TARGET:=bridge_${GOOS}_${REVISION}.tgz TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
build: ${TGZ_TARGET} build: ${TGZ_TARGET}
build-nogui: build-nogui:
go build ${BUILD_FLAGS_NOGUI} -o Desktop-Bridge cmd/Desktop-Bridge/main.go go build ${BUILD_FLAGS_NOGUI} -o Desktop-Bridge cmd/Desktop-Bridge/main.go
${TGZ_TARGET}: ${DEPLOY_DIR}/${GOOS} ${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
rm -f $@ rm -f $@
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${GOOS} cd ${DEPLOY_DIR} && tar czf ../../../$@ ${TARGET_OS}
${DEPLOY_DIR}/linux: ${EXE_TARGET} ${DEPLOY_DIR}/linux: ${EXE_TARGET}
cp -pf ./internal/frontend/share/icons/logo.svg ${DEPLOY_DIR}/linux/ cp -pf ./internal/frontend/share/icons/logo.svg ${DEPLOY_DIR}/linux/
@ -55,42 +60,52 @@ ${DEPLOY_DIR}/darwin: ${EXE_TARGET}
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}"
${DEPLOY_DIR}/windows: ${EXE_TARGET} ${DEPLOY_DIR}/windows: ${EXE_TARGET}
cp ./internal/frontend/share/icons/logo.ico ${DEPLOY_DIR}/windows/ cp ./internal/frontend/share/icons/logo.ico ${DEPLOY_DIR}/windows/
cp LICENSE ${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 ${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 . cp cmd/Desktop-Bridge/main.go .
qtdeploy ${BUILD_FLAGS} build desktop qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
mv deploy cmd/Desktop-Bridge mv deploy cmd/Desktop-Bridge
rm -rf ${GOOS} main.go rm -rf ${TARGET_OS} main.go
logo.ico: ./internal/frontend/share/icons/logo.ico logo.ico: ./internal/frontend/share/icons/logo.ico
cp $^ . cp $^ .
icon.rc: ./internal/frontend/share/icon.rc icon.rc: ./internal/frontend/share/icon.rc
cp $^ . cp $^ .
./internal/frontend/qt/icon_windows.syso: ./internal/frontend/share/icon.rc logo.ico ./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 icon_windows.syso: ./internal/frontend/qt/icon_windows.syso
cp $^ . cp $^ .
## Rules for therecipe/qt ## Rules for therecipe/qt
.PHONY: prepare-vendor update-vendor .PHONY: prepare-vendor update-vendor
THERECIPE_QTVER:=$(shell grep "github.com/therecipe/qt " go.mod | sed -r 's;.* v[0-9\.]+-[0-9]+-([a-f0-9]*).*;\1;') THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
THERECIPE_ENV:=github.com/therecipe/env_${GOOS}_amd64_513
# vendor folder will be deleted by gomod hence we cache the big repo # vendor folder will be deleted by gomod hence we cache the big repo
# therecipe/env in order to download it only once # therecipe/env in order to download it only once
vendor-cache/${THERECIPE_ENV}: vendor-cache/${THERECIPE_ENV}:
git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV} git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV}
# The command used to make symlinks is different on windows.
# So if the GOOS is windows and we aren't crossbuilding (in which case the host os would still be *nix)
# we need to change the LINKCMD to something windowsy.
LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV} LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV}
ifeq "${GOOS}" "windows" ifeq "${GOOS}" "windows"
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV} WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
LINKCMD:=cmd //c 'mklink $(subst /,\,vendor\${THERECIPE_ENV} ${WINDIR})' LINKCMD:=cmd //c 'mklink $(subst /,\,vendor\${THERECIPE_ENV} ${WINDIR})'
endif endif
prepare-vendor: prepare-vendor:
@ -104,7 +119,7 @@ update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
## Dev dependencies ## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated .PHONY: install-devel-tools install-linter install-go-mod-outdated
LINTVER:="v1.23.6" LINTVER:="v1.27.0"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh" LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -122,12 +137,15 @@ install-go-mod-outdated:
## Checks, mocks and docs ## Checks, mocks and docs
.PHONY: check-has-go check-license test bench coverage mocks lint updates doc .PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc
check-has-go: check-has-go:
@which go || (echo "Install Go-lang!" && exit 1) @which go || (echo "Install Go-lang!" && exit 1)
check-license: add-license:
find . -not -path "./vendor/*" -not -name "*mock*.go" -regextype posix-egrep -regex ".*\.go|.*\.qml" -exec grep -L "Copyright (c) 2020 Proton Technologies AG" {} \; ./utils/missing_license.sh add
change-copyright-year:
./utils/missing_license.sh change-year
test: gofiles test: gofiles
@# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests. @# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests.
@ -138,9 +156,11 @@ test: gofiles
./internal/frontend/autoconfig/... \ ./internal/frontend/autoconfig/... \
./internal/frontend/cli/... \ ./internal/frontend/cli/... \
./internal/imap/... \ ./internal/imap/... \
./internal/metrics/... \
./internal/preferences/... \ ./internal/preferences/... \
./internal/smtp/... \ ./internal/smtp/... \
./internal/store/... \ ./internal/store/... \
./internal/users/... \
./pkg/... ./pkg/...
bench: bench:
@ -152,11 +172,17 @@ coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks: mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/bridge Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer > internal/bridge/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,BridgeUser > internal/store/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
lint: lint: lint-golang lint-license
lint-license:
./utils/missing_license.sh check
lint-golang:
which golangci-lint || $(MAKE) install-linter which golangci-lint || $(MAKE) install-linter
golangci-lint run ./... golangci-lint run ./...
@ -210,3 +236,4 @@ clean: clean-frontend-qt
rm -rf vendor-cache rm -rf vendor-cache
rm -rf cmd/Desktop-Bridge/deploy rm -rf cmd/Desktop-Bridge/deploy
rm -f build last.log mem.pprof rm -f build last.log mem.pprof
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso

View File

@ -6,6 +6,7 @@ For a detailed build information see [BUILDS](./BUILDS.md).
For licensing information see [COPYING](./COPYING.md). For licensing information see [COPYING](./COPYING.md).
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md). For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
## Description ## Description
ProtonMail Bridge for e-mail clients. ProtonMail Bridge for e-mail clients.
@ -31,15 +32,16 @@ Windows, Bridge uses native credential managers. On Linux, use
or or
[pass](https://www.passwordstore.org/). [pass](https://www.passwordstore.org/).
## Environment Variables ## Environment Variables
### Bridge application ### Bridge application
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable. - `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
### Dev build or run ### Dev build or run
- `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 - `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
- `VERBOSITY`: set log level used during test time and by the makefile. - `VERBOSITY`: set log level used during test time and by the makefile
- `VERSION`: set the bridge app version used during testing or building.
### Integration testing ### Integration testing
- `TEST_ENV`: set which env to use (fake or live) - `TEST_ENV`: set which env to use (fake or live)
@ -48,5 +50,34 @@ or
- `FEATURES`: set feature dir, file or scenario to test - `FEATURES`: set feature dir, file or scenario to test
## Files
### Database
The database stores metadata necessary for presenting messages and mailboxes to an email client:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
### Preferences
User preferences are stored in json at the following location:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/prefs.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/prefs.json`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\prefs.json`
### IMAP Cache
The currently subscribed mailboxes are held in a json file:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
### Lock file
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
### TLS Certificate and Key
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`

View File

@ -47,16 +47,17 @@ import (
"github.com/ProtonMail/proton-bridge/internal/api" "github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend" "github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/imap" "github.com/ProtonMail/proton-bridge/internal/imap"
"github.com/ProtonMail/proton-bridge/internal/pmapifactory"
"github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp" "github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/args" "github.com/ProtonMail/proton-bridge/pkg/args"
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/updates" "github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/allan-simon/go-singleinstance" "github.com/allan-simon/go-singleinstance"
"github.com/getsentry/raven-go" "github.com/getsentry/raven-go"
@ -68,45 +69,28 @@ import (
// Different number will drop old files and create new ones. // Different number will drop old files and create new ones.
const cacheVersion = "c11" const cacheVersion = "c11"
// Following variables are set via ldflags during build.
var ( var (
// Version of the build. log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
Version = "" //nolint[gochecknoglobals]
// Revision is current hash of the build.
Revision = "" //nolint[gochecknoglobals]
// BuildTime stamp of the build.
BuildTime = "" //nolint[gochecknoglobals]
// AppShortName to make setup
AppShortName = "bridge" //nolint[gochecknoglobals]
// DSNSentry client keys to be able to report crashes to Sentry
DSNSentry = "" //nolint[gochecknoglobals]
)
var (
longVersion = Version + " (" + Revision + ")" //nolint[gochecknoglobals]
buildVersion = longVersion + " " + BuildTime //nolint[gochecknoglobals]
log = config.GetLogEntry("main") //nolint[gochecknoglobals]
// How many crashes in a row. // How many crashes in a row.
numberOfCrashes = 0 //nolint[gochecknoglobals] numberOfCrashes = 0 //nolint[gochecknoglobals]
// After how many crashes bridge gives up starting. // After how many crashes bridge gives up starting.
maxAllowedCrashes = 10 //nolint[gochecknoglobals] maxAllowedCrashes = 10 //nolint[gochecknoglobals]
) )
func main() { 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") log.WithError(err).Errorln("Can not setup sentry DSN")
} }
raven.SetRelease(Revision) raven.SetRelease(constants.Revision)
bridge.UpdateCurrentUserAgent(Version, runtime.GOOS, "", "")
args.FilterProcessSerialNumberFromArgs() args.FilterProcessSerialNumberFromArgs()
filterRestartNumberFromArgs() filterRestartNumberFromArgs()
app := cli.NewApp() app := cli.NewApp()
app.Name = "Protonmail Bridge" app.Name = "Protonmail Bridge"
app.Version = buildVersion app.Version = constants.BuildVersion
app.Flags = []cli.Flag{ app.Flags = []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "log-level, l", Name: "log-level, l",
@ -135,13 +119,13 @@ func main() {
// Always log the basic info about current bridge. // Always log the basic info about current bridge.
logrus.SetLevel(logrus.InfoLevel) logrus.SetLevel(logrus.InfoLevel)
log.WithField("version", Version). log.WithField("version", constants.Version).
WithField("revision", Revision). WithField("revision", constants.Revision).
WithField("runtime", runtime.GOOS). WithField("runtime", runtime.GOOS).
WithField("build", BuildTime). WithField("build", constants.BuildTime).
WithField("args", os.Args). WithField("args", os.Args).
WithField("appLong", app.Name). WithField("appLong", app.Name).
WithField("appShort", AppShortName). WithField("appShort", constants.AppShortName).
Info("Run app") Info("Run app")
if err := app.Run(os.Args); err != nil { if err := app.Run(os.Args); err != nil {
log.Error("Program exited with error: ", err) log.Error("Program exited with error: ", err)
@ -174,7 +158,7 @@ func (ph *panicHandler) HandlePanic() {
// IMPORTANT: ***Read the comments before CHANGING the order *** // IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen] func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ... // We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(AppShortName, Version, Revision, cacheVersion) cfg := config.New(constants.AppShortName, constants.Version, constants.Revision, cacheVersion)
// We want to know about any problem. Our PanicHandler calls sentry which is // We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash // not dependent on anything else. If that fails, it tries to create crash
@ -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. // It's safe to get version JSON file even when other instance is running.
// (thus we put it before check of presence of other Bridge instance). // (thus we put it before check of presence of other Bridge instance).
updates := updates.New(AppShortName, Version, Revision, BuildTime, bridge.ReleaseNotes, bridge.ReleaseFixedBugs, cfg.GetUpdateDir()) updates := updates.New(
constants.AppShortName,
constants.Version,
constants.Revision,
constants.BuildTime,
bridge.ReleaseNotes,
bridge.ReleaseFixedBugs,
cfg.GetUpdateDir(),
)
if dir := context.GlobalString("version-json"); dir != "" { if dir := context.GlobalString("version-json"); dir != "" {
generateVersionFiles(updates, dir) generateVersionFiles(updates, dir)
return nil return nil
@ -268,14 +261,19 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
eventListener := listener.New() eventListener := listener.New()
events.SetupEvents(eventListener) events.SetupEvents(eventListener)
credentialsStore, credentialsError := credentials.NewStore() credentialsStore, credentialsError := credentials.NewStore("bridge")
if credentialsError != nil { if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError) log.Error("Could not get credentials store: ", credentialsError)
} }
pmapiClientFactory := pmapifactory.New(cfg, eventListener) cm := pmapi.NewClientManager(cfg.GetAPIConfig())
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, Version, pmapiClientFactory, credentialsStore) // Different build types have different roundtrippers (e.g. we want to enable
// TLS fingerprint checks in production builds). GetRoundTripper has a different
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, cm, credentialsStore)
imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance) imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance)
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance) smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
@ -321,7 +319,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
} }
showWindowOnStart := !context.GlobalBool("no-window") showWindowOnStart := !context.GlobalBool("no-window")
frontend := frontend.New(Version, buildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend) frontend := frontend.New(constants.Version, constants.BuildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
// Last part is to start everything. // Last part is to start everything.
log.Debug("Starting frontend...") log.Debug("Starting frontend...")
@ -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. // It will happen only when c10/prefs.json exists and c11/prefs.json not.
// No configuration changed between c10 and c11 versions. // No configuration changed between c10 and c11 versions.
func migratePreferencesFromC10(cfg *config.Config) { func migratePreferencesFromC10(cfg *config.Config) {
pref10Path := config.New(AppShortName, Version, Revision, "c10").GetPreferencesPath() pref10Path := config.New(constants.AppShortName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
if _, err := os.Stat(pref10Path); os.IsNotExist(err) { if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped") log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
return return
@ -359,7 +357,7 @@ func migratePreferencesFromC10(cfg *config.Config) {
return return
} }
err = ioutil.WriteFile(pref11Path, data, 0644) //nolint[gosec] err = ioutil.WriteFile(pref11Path, data, 0600)
if err != nil { if err != nil {
log.WithError(err).Error("Problem to migrate preferences") log.WithError(err).Error("Problem to migrate preferences")
return return

32
go.mod
View File

@ -5,11 +5,10 @@ go 1.13
// These dependencies are `replace`d below, so the version numbers should be ignored. // These dependencies are `replace`d below, so the version numbers should be ignored.
// They are in a separate require block to highlight this. // They are in a separate require block to highlight this.
require ( require (
github.com/docker/docker-credential-helpers v0.0.0-00010101000000-000000000000 github.com/docker/docker-credential-helpers v0.6.3
github.com/emersion/go-imap v0.0.0-20171113213225-939ec3994dbe
github.com/emersion/go-imap-quota v0.0.0-20171113212021-e883a2bc54d6
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67 github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
) )
require ( require (
@ -17,9 +16,9 @@ require (
github.com/ProtonMail/go-appdir v1.1.0 github.com/ProtonMail/go-appdir v1.1.0
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed github.com/ProtonMail/gopenpgp/v2 v2.0.1
github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
@ -28,12 +27,13 @@ require (
github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cucumber/godog v0.8.1 github.com/cucumber/godog v0.8.1
github.com/danieljoos/wincred v1.0.2 // indirect github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42 github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89 github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
github.com/emersion/go-imap-move v0.0.0-20161227183138-88aef42b0f1d github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 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-sasl v0.0.0-20191210011802-430746ea8b9b
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
@ -58,22 +58,18 @@ require (
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 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/twinj/uuid v1.0.0 // indirect
github.com/urfave/cli v1.22.3 github.com/urfave/cli v1.22.3
go.etcd.io/bbolt v1.3.3 go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/net v0.0.0-20200301022130-244492dfa37a golang.org/x/net v0.0.0-20200301022130-244492dfa37a
golang.org/x/text v0.3.2 golang.org/x/text v0.3.2
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
) )
replace ( replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.0.0 github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f github.com/emersion/go-imap => github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843
github.com/emersion/go-imap-quota => github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c
) )

95
go.sum
View File

@ -3,32 +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/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 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f h1:cFhATQTJGK2iZ0dc+jRhr75mh6bsc5Ug6NliaBya8Kw= github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/docker-credential-helpers v1.0.0 h1:0DQXbZNvUszWgXUuP7TzvQdwnkK1D5Zf/glBgCFJFCk= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.0.0/go.mod h1:R1gQindzdYFcWJuuGXteYHDJzUCVtyU+EpEqp9aWcFs= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/go-appdir v1.0.0 h1:PZXQ0HkveuEugga3LeDycxWtybrXQfKR0ThxURd6ojw=
github.com/ProtonMail/go-appdir v1.0.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig= github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
github.com/ProtonMail/go-appdir v1.1.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc= github.com/ProtonMail/go-appdir v1.1.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h1:YsSJ/mvZFYydQm/hRrt8R8UtgETixN2y3LK98f5LT60= github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h1:YsSJ/mvZFYydQm/hRrt8R8UtgETixN2y3LK98f5LT60=
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg= github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f h1:QkLm4yfhBQuBxrC46Vhy2sonOWVrwIJo5bgKpA82+TY= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f/go.mod h1:+m2uLXghuYktgE/vc5AkmCxx1qhu33ZKHFWg1cGZPD0= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee h1:Q/nK7A9xzUimAZsQDa/yaw3xW9PkTTnJnkT5wAkXrmI= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de h1:+LA9teDYUwGkBvg0kqZPZetmxIv1r7s9/npBP1yzKs0=
github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de/go.mod h1:85zbnYVWIY7//iScX9fnB/kKOGH9B86YPqtpr7f1i7A=
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72 h1:hGCc4Oc2fD3I5mNnZ1VlREncVc9EXJF8dxW3sw16gWM=
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE= github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI= github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed h1:3gib6hGF61VfRu7cqqkODyRUgES5uF/fkLQanPPJiO8= github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed/go.mod h1:NstNbZx1OIoyq+2qHAFLwDFpHbMk8L2i2Vr+LioJ3/g= github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
@ -37,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/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/aslakhellesoy/gox v1.0.100/go.mod h1:AJl542QsKKG96COVsv0N74HHzVQgDIQPceVUh1aeU2M=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA= github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
@ -48,39 +41,27 @@ 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/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 h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cucumber/gherkin-go/v11 v11.0.0 h1:cwVwN1Qn2VRSfHZNLEh5x00tPBmZcjATBWDpxsR5Xug=
github.com/cucumber/gherkin-go/v11 v11.0.0/go.mod h1:CX33k2XU2qog4e+TFjOValoq6mIUq0DmVccZs238R9w=
github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8= github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8=
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/cucumber/godog v0.9.0 h1:QOb8wyC7f+FVFXzY3RdgowwJUb4WeJfqbnQqaH4jp+A= github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
github.com/cucumber/godog v0.9.0/go.mod h1:roWCHkpeK6UTOyIRRl7IR+fgfBeZ4vZR7OSq2J/NbM4= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/cucumber/messages-go/v10 v10.0.1/go.mod h1:kA5T38CBlBbYLU12TIrJ4fk4wSkVVOgyh7Enyy8WnSg=
github.com/cucumber/messages-go/v10 v10.0.3 h1:m/9SD/K/A15WP7i1aemIv7cwvUw+viS51Ui5HBw1cdE=
github.com/cucumber/messages-go/v10 v10.0.3/go.mod h1:9jMZ2Y8ZxjLY6TG2+x344nt5rXstVVDYSdS5ySfI1WY=
github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42 h1:3TeZ5gy3We/LVL0sqmGhM8dFDTSM7Hyj7PMIdl6OTs4= github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ= github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89 h1:AzbVhcrxgJO5MfSvzG5q4IfrYVm0Jw4AHNPz47+DiR0= github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78= github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-imap-move v0.0.0-20161227173100-88aef42b0f1d h1:STRZFC+5HZITdsSFkhFfyYRb+tkiTwhxFz3sRW1lYjk=
github.com/emersion/go-imap-move v0.0.0-20161227173100-88aef42b0f1d/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-move v0.0.0-20161227183138-88aef42b0f1d h1:E/ezdheD3QUe47cM0LpAPuJ6Pk1x0EFDmjoysaZhtaw=
github.com/emersion/go-imap-move v0.0.0-20161227183138-88aef42b0f1d/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-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 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4= 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 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-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 h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
@ -97,10 +78,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-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 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 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 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
@ -114,8 +91,8 @@ 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/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 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 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/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE= github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jaytaylor/html2text v0.0.0-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 h1:j0UEFmS7wSjAwKEIkgKBn8PRDfjcuggzr93R9wk53nQ=
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
@ -125,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/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 h1:1XZArHAPddaXKbg51etNbCjkNUkKgSa0s8dSz2LYB2g=
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc= 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.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 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/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 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 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 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
@ -146,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/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 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 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 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/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 h1:qsqscDgSJy+HqgMTR+3NwjYJBbp1+honwDsszLoS+pA=
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
@ -183,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/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 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= 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 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU= github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 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.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -211,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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
@ -232,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= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -32,10 +32,11 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/sirupsen/logrus"
) )
var ( var (
log = config.GetLogEntry("api") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "api") //nolint[gochecknoglobals]
) )
type apiServer struct { type apiServer struct {

View File

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

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Thu Apr 16 13:43:04 CEST 2020. DO NOT EDIT. // Code generated by ./credits.sh at Wed 29 Jul 2020 06:44:08 AM CEST. DO NOT EDIT.
package bridge package bridge
const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-imap-quota;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/danieljoos/wincred;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" const Credits = "github.com/0xAX/notificator;github.com/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;"

View File

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

View File

@ -15,20 +15,17 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 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 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 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.
Faster and more resilient mail synchronization process, especially for large mailboxes User interaction improvements: Some smaller improvements in specific cases to make the interaction with Proton Bridge clearer for the user
Added "Alternate Routing" feature to mitigate blocking of Proton Servers 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.
Added synchronization of draft folder General stability improvements: Improvements to the behavior of the application under various unstable internet conditions.
• Improved event handling when there are frequent changes
• Security improvements for loading dependent libraries
• Minor UI & API communication tweaks
` `
const ReleaseFixedBugs = `• Fixed rare case of sending the same message multiple times in Outlook const ReleaseFixedBugs = `• Fixed a slew of smaller bugs and some conditions which could cause the application to crash.
Fixed bug in macOS update process; available from next update The full changelog can be found at https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
` `

View File

@ -0,0 +1,69 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
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)
}

View File

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

View File

@ -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")
}

View File

@ -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())
}

View File

@ -40,6 +40,7 @@ const (
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient" NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
UpgradeApplicationEvent = "upgradeApplication" UpgradeApplicationEvent = "upgradeApplication"
TLSCertIssue = "tlsCertPinningIssue" TLSCertIssue = "tlsCertPinningIssue"
IMAPTLSBadCert = "imapTLSBadCert"
// LogoutEventTimeout is the minimum time to permit between logout events being sent. // LogoutEventTimeout is the minimum time to permit between logout events being sent.
LogoutEventTimeout = 3 * time.Minute LogoutEventTimeout = 3 * time.Minute

View File

@ -26,10 +26,11 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
"github.com/sirupsen/logrus"
) )
var ( var (
log = config.GetLogEntry("frontend/cli") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "frontend/cli") //nolint[gochecknoglobals]
) )
type frontendCLI struct { type frontendCLI struct {

View File

@ -22,9 +22,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/connection"
"github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
) )
@ -42,7 +40,7 @@ func (f *frontendCLI) restart(c *ishell.Context) {
} }
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) { func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if connection.CheckInternetConnection() == nil { if f.bridge.CheckConnection() == nil {
f.Println("Internet connection is available.") f.Println("Internet connection is available.")
} else { } else {
f.Println("Can not contact server please check you internet connection.") f.Println("Can not contact 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.") f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") { if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.preferences.SetBool(preferences.AllowProxyKey, false) f.preferences.SetBool(preferences.AllowProxyKey, false)
bridge.DisallowDoH() f.bridge.DisallowProxy()
} }
} else { } else {
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.") f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") { if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
f.preferences.SetBool(preferences.AllowProxyKey, true) f.preferences.SetBool(preferences.AllowProxyKey, true)
bridge.AllowDoH() f.bridge.AllowProxy()
} }
} }
} }

View File

@ -26,10 +26,11 @@ import (
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
) )
var ( var (
log = config.GetLogEntry("frontend") // nolint[unused] log = logrus.WithField("pkg", "frontend") // nolint[unused]
) )
// Frontend is an interface to be implemented by each frontend type (cli, gui, html). // Frontend is an interface to be implemented by each frontend type (cli, gui, html).

View File

@ -237,6 +237,14 @@ Item {
winMain.tlsBarState="notOK" winMain.tlsBarState="notOK"
} }
onShowIMAPCertTroubleshoot : {
go.notifyBubble(1, qsTr(
"Bridge was unable to establish a connection with your Email client. <br> <a href=\"https://protonmail.com/support/knowledge-base/bridge-ssl-connection-issue\">Learn more</a> <br>",
"notification message"
))
}
} }
Timer { Timer {

View File

@ -110,7 +110,7 @@ Window {
ListElement { title: "Internet off" } ListElement { title: "Internet off" }
ListElement { title: "NeedUpdate" } ListElement { title: "NeedUpdate" }
ListElement { title: "UpToDate" } ListElement { title: "UpToDate" }
ListElement { title: "ForceUpdate" } ListElement { title: "ForceUpdate" }
ListElement { title: "Linux" } ListElement { title: "Linux" }
ListElement { title: "Windows" } ListElement { title: "Windows" }
ListElement { title: "Macos" } ListElement { title: "Macos" }
@ -122,6 +122,7 @@ Window {
ListElement { title: "Minimize this" } ListElement { title: "Minimize this" }
ListElement { title: "SendAlertPopup" } ListElement { title: "SendAlertPopup" }
ListElement { title: "TLSCertError" } ListElement { title: "TLSCertError" }
ListElement { title: "IMAPCertError" }
} }
ListView { ListView {
@ -208,6 +209,9 @@ Window {
case "TLSCertError" : case "TLSCertError" :
go.showCertIssue() go.showCertIssue()
break; break;
case "IMAPCertError" :
go.showIMAPCertTroubleshoot()
break;
default : default :
console.log("Not implemented " + data) console.log("Not implemented " + data)
} }
@ -310,6 +314,7 @@ Window {
signal failedAutostartCode(string code) signal failedAutostartCode(string code)
signal showCertIssue() signal showCertIssue()
signal showIMAPCertTroubleshoot()
signal updateFinished(bool hasError) signal updateFinished(bool hasError)

View File

@ -45,10 +45,11 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent" "github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/sirupsen/logrus"
//"github.com/ProtonMail/proton-bridge/pkg/keychain" //"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener" "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/ProtonMail/proton-bridge/pkg/updates"
"github.com/kardianos/osext" "github.com/kardianos/osext"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
@ -58,7 +59,7 @@ import (
"github.com/therecipe/qt/widgets" "github.com/therecipe/qt/widgets"
) )
var log = config.GetLogEntry("frontend-qt") var log = logrus.WithField("pkg", "frontend-qt")
var accountMutex = &sync.Mutex{} var accountMutex = &sync.Mutex{}
// API between Bridge and Qt. // API between Bridge and Qt.
@ -85,7 +86,7 @@ type FrontendQt struct {
programName string // Program name (shown in taskbar). programName string // Program name (shown in taskbar).
programVer string // Program version (shown in help). programVer string // Program version (shown in help).
authClient bridge.PMAPIProvider authClient pmapi.Client
auth *pmapi.Auth auth *pmapi.Auth
@ -187,6 +188,7 @@ func (s *FrontendQt) watchEvents() {
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent) updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent)
newUserCh := s.getEventChannel(events.UserRefreshEvent) newUserCh := s.getEventChannel(events.UserRefreshEvent)
certIssue := s.getEventChannel(events.TLSCertIssue) certIssue := s.getEventChannel(events.TLSCertIssue)
imapCertIssue := s.getEventChannel(events.IMAPTLSBadCert)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -226,6 +228,8 @@ func (s *FrontendQt) watchEvents() {
s.Qml.LoadAccounts() s.Qml.LoadAccounts()
case <-certIssue: case <-certIssue:
s.Qml.ShowCertIssue() s.Qml.ShowCertIssue()
case <-imapCertIssue:
s.Qml.ShowIMAPCertTroubleshoot()
} }
} }
} }
@ -527,11 +531,11 @@ func (s *FrontendQt) toggleAllowProxy() {
if s.preferences.GetBool(preferences.AllowProxyKey) { if s.preferences.GetBool(preferences.AllowProxyKey) {
s.preferences.SetBool(preferences.AllowProxyKey, false) s.preferences.SetBool(preferences.AllowProxyKey, false)
bridge.DisallowDoH() s.bridge.DisallowProxy()
s.Qml.SetIsProxyAllowed(false) s.Qml.SetIsProxyAllowed(false)
} else { } else {
s.preferences.SetBool(preferences.AllowProxyKey, true) s.preferences.SetBool(preferences.AllowProxyKey, true)
bridge.AllowDoH() s.bridge.AllowProxy()
s.Qml.SetIsProxyAllowed(true) s.Qml.SetIsProxyAllowed(true)
} }
} }
@ -568,7 +572,7 @@ func (s *FrontendQt) isSMTPSTARTTLS() bool {
} }
func (s *FrontendQt) checkInternet() { func (s *FrontendQt) checkInternet() {
s.Qml.SetConnectionStatus(IsInternetAvailable()) s.Qml.SetConnectionStatus(s.bridge.CheckConnection() == nil)
} }
func (s *FrontendQt) switchAddressModeUser(iAccount int) { func (s *FrontendQt) switchAddressModeUser(iAccount int) {

View File

@ -26,9 +26,10 @@ import (
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
) )
var log = config.GetLogEntry("frontend-nogui") //nolint[gochecknoglobals] var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
type FrontendHeadless struct{} type FrontendHeadless struct{}

View File

@ -25,7 +25,6 @@ import (
"os/exec" "os/exec"
"time" "time"
"github.com/ProtonMail/proton-bridge/pkg/connection"
"github.com/therecipe/qt/core" "github.com/therecipe/qt/core"
) )
@ -64,10 +63,6 @@ func PauseLong() {
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
} }
func IsInternetAvailable() bool {
return connection.CheckInternetConnection() == nil
}
// FIXME: Not working in test... // FIXME: Not working in test...
func WaitForEnter() { func WaitForEnter() {
log.Print("Press 'Enter' to continue...") log.Print("Press 'Enter' to continue...")

View File

@ -135,6 +135,7 @@ type GoQMLInterface struct {
_ func(x, y float32) `slot:"saveOutgoingNoEncPopupCoord"` _ func(x, y float32) `slot:"saveOutgoingNoEncPopupCoord"`
_ func(recipient string) `signal:"showNoActiveKeyForRecipient"` _ func(recipient string) `signal:"showNoActiveKeyForRecipient"`
_ func() `signal:"showCertIssue"` _ func() `signal:"showCertIssue"`
_ func() `signal:"ShowIMAPCertTroubleshoot"`
_ func() `slot:"startUpdate"` _ func() `slot:"startUpdate"`
_ func(hasError bool) `signal:"updateFinished"` _ func(hasError bool) `signal:"updateFinished"`

17
internal/frontend/share/icons/export.sh Executable file → Normal file
View File

@ -1,5 +1,22 @@
#!/bin/bash #!/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 # create bitmaps
for shape in rounded rectangle for shape in rounded rectangle
do do

View File

@ -20,7 +20,7 @@ package types
import ( import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "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" "github.com/ProtonMail/proton-bridge/pkg/updates"
) )
@ -45,13 +45,16 @@ type NoEncConfirmator interface {
type Bridger interface { type Bridger interface {
GetCurrentClient() string GetCurrentClient() string
SetCurrentOS(os string) SetCurrentOS(os string)
Login(username, password string) (bridge.PMAPIProvider, *pmapi.Auth, error) Login(username, password string) (pmapi.Client, *pmapi.Auth, error)
FinishLogin(client bridge.PMAPIProvider, auth *pmapi.Auth, mailboxPassword string) (BridgeUser, error) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (BridgeUser, error)
GetUsers() []BridgeUser GetUsers() []BridgeUser
GetUser(query string) (BridgeUser, error) GetUser(query string) (BridgeUser, error)
DeleteUser(userID string, clearCache bool) error DeleteUser(userID string, clearCache bool) error
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
ClearData() error ClearData() error
AllowProxy()
DisallowProxy()
CheckConnection() error
} }
// BridgeUser is an interface of user needed by frontend. // 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} 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) return b.Bridge.FinishLogin(client, auth, mailboxPassword)
} }

View File

@ -27,7 +27,9 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-imap"
goIMAPBackend "github.com/emersion/go-imap/backend" goIMAPBackend "github.com/emersion/go-imap/backend"
"github.com/sirupsen/logrus"
) )
type panicHandler interface { type panicHandler interface {
@ -37,7 +39,7 @@ type panicHandler interface {
type imapBackend struct { type imapBackend struct {
panicHandler panicHandler panicHandler panicHandler
bridge bridger bridge bridger
updates chan interface{} updates chan goIMAPBackend.Update
eventListener listener.Listener eventListener listener.Listener
users map[string]*imapUser users map[string]*imapUser
@ -79,7 +81,7 @@ func newIMAPBackend(
return &imapBackend{ return &imapBackend{
panicHandler: panicHandler, panicHandler: panicHandler,
bridge: bridge, bridge: bridge,
updates: make(chan interface{}), updates: make(chan goIMAPBackend.Update),
eventListener: eventListener, eventListener: eventListener,
users: map[string]*imapUser{}, users: map[string]*imapUser{},
@ -150,7 +152,7 @@ func (ib *imapBackend) deleteUser(address string) {
} }
// Login authenticates a user. // 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. // Called from go-imap in goroutines - we need to handle panics for each function.
defer ib.panicHandler.HandlePanic() defer ib.panicHandler.HandlePanic()
@ -179,7 +181,7 @@ func (ib *imapBackend) Login(username, password string) (goIMAPBackend.User, err
} }
// Updates returns a channel of updates for IMAP IDLE extension. // 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. // Called from go-imap in goroutines - we need to handle panics for each function.
defer ib.panicHandler.HandlePanic() defer ib.panicHandler.HandlePanic()
@ -218,3 +220,11 @@ func (ib *imapBackend) monitorDisconnectedUsers() {
ib.deleteUser(address) ib.deleteUser(address)
} }
} }
func (ib *imapBackend) upgradeError(err error) {
logrus.WithError(err).Error("IMAP connection couldn't be upgraded to TLS during STARTTLS")
if strings.Contains(err.Error(), "remote error: tls: bad certificate") {
ib.eventListener.Emit(events.IMAPTLSBadCert, err.Error())
}
}

View File

@ -19,6 +19,8 @@ package imap
import ( import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
type configProvider interface { type configProvider interface {
@ -43,7 +45,7 @@ type bridgeUser interface {
Logout() error Logout() error
CloseConnection(address string) CloseConnection(address string)
GetStore() storeUserProvider GetStore() storeUserProvider
GetTemporaryPMAPIClient() bridge.PMAPIProvider GetTemporaryPMAPIClient() pmapi.Client
} }
type bridgeWrap struct { type bridgeWrap struct {
@ -66,10 +68,10 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
} }
type bridgeUserWrap struct { type bridgeUserWrap struct {
*bridge.User *users.User
} }
func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap { func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
return &bridgeUserWrap{User: bridgeUser} return &bridgeUserWrap{User: bridgeUser}
} }

View File

@ -17,7 +17,7 @@
package imap package imap
import "github.com/ProtonMail/proton-bridge/pkg/config" import "github.com/sirupsen/logrus"
const ( const (
fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP). fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP).
@ -31,5 +31,5 @@ const (
) )
var ( var (
log = config.GetLogEntry("imap") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "imap") //nolint[gochecknoglobals]
) )

View File

@ -106,7 +106,7 @@ func (im *imapMailbox) getFlags() []string {
// //
// It always returns the state of DB (which could be different to server status). // 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. // 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. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() defer im.panicHandler.HandlePanic()
@ -125,11 +125,17 @@ func (im *imapMailbox) Status(items []string) (*imap.MailboxStatus, error) {
message.ThunderbirdNonJunkFlag, message.ThunderbirdNonJunkFlag,
} }
dbTotal, dbUnread, err := im.storeMailbox.GetCounts() dbTotal, dbUnread, dbUnreadSeqNum, err := im.storeMailbox.GetCounts()
l.Debugln("DB: total", dbTotal, "unread", dbUnread, "err", err) l.WithFields(logrus.Fields{
"total": dbTotal,
"unread": dbUnread,
"unreadSeqNum": dbUnreadSeqNum,
"err": err,
}).Debug("DB counts")
if err == nil { if err == nil {
status.Messages = uint32(dbTotal) status.Messages = uint32(dbTotal)
status.Unseen = uint32(dbUnread) status.Unseen = uint32(dbUnread)
status.UnseenSeqNum = uint32(dbUnreadSeqNum)
} }
if status.UidNext, err = im.storeMailbox.GetNextUID(); err != nil { 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 return status, nil
} }
// Subscribe adds the mailbox to the server's set of "active" or "subscribed" mailboxes. // SetSubscribed adds or removes the mailbox to the server's set of "active"
func (im *imapMailbox) Subscribe() error { // or "subscribed" mailboxes.
func (im *imapMailbox) SetSubscribed(subscribed bool) error {
// Called from go-imap in goroutines - we need to handle panics for each function. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() defer im.panicHandler.HandlePanic()
label := im.storeMailbox.LabelID() label := im.storeMailbox.LabelID()
if !im.user.isSubscribed(label) { if subscribed && !im.user.isSubscribed(label) {
im.user.removeFromCache(SubscriptionException, label) im.user.removeFromCache(SubscriptionException, label)
} }
if !subscribed && im.user.isSubscribed(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) {
im.user.addToCache(SubscriptionException, label) im.user.addToCache(SubscriptionException, label)
} }
return nil return nil
} }

View File

@ -29,9 +29,10 @@ import (
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"text/template"
"time" "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/cache"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message" "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") return errors.New("no available address for encryption")
} }
m.AddressID = addr.ID 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. // Handle imported messages which have no "Sender" address.
// This sometimes occurs with outlook which reports errors as imported emails or for drafts. // 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. // 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") 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 // 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 { if len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1] lastReference := referenceList[len(referenceList)-1]
// In case we are using a mail client which corrupts headers, try "References" too. // In case we are using a mail client which corrupts headers, try "References" too.
re := regexp.MustCompile("<[a-zA-Z0-9-_=]*@protonmail.internalid>") re := regexp.MustCompile(pmapi.InternalReferenceFormat)
match := re.FindString(lastReference) match := re.FindStringSubmatch(lastReference)
if match != "" { if len(match) > 0 {
internalID = match[1 : len(match)-len("@protonmail.internalid>")] 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) 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{} b := &bytes.Buffer{}
// Overwrite content for main header for import. // 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. // Create encrypted writer.
pgpMessage, err := kr.Encrypt(pmcrypto.NewPlainMessage(data), nil) pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
if err != nil { if err != nil {
return err 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) 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") im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message")
seqNum, err := storeMessage.SequenceNumber() seqNum, err := storeMessage.SequenceNumber()
@ -278,9 +279,9 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []str
msg = imap.NewMessage(seqNum, items) msg = imap.NewMessage(seqNum, items)
for _, item := range items { for _, item := range items {
switch item { switch item {
case imap.EnvelopeMsgAttr: case imap.FetchEnvelope:
msg.Envelope = message.GetEnvelope(m) msg.Envelope = message.GetEnvelope(m)
case imap.BodyMsgAttr, imap.BodyStructureMsgAttr: case imap.FetchBody, imap.FetchBodyStructure:
var structure *message.BodyStructure var structure *message.BodyStructure
if structure, _, err = im.getBodyStructure(storeMessage); err != nil { if structure, _, err = im.getBodyStructure(storeMessage); err != nil {
return return
@ -288,11 +289,11 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []str
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil { if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
return return
} }
case imap.FlagsMsgAttr: case imap.FetchFlags:
msg.Flags = message.GetFlags(m) msg.Flags = message.GetFlags(m)
case imap.InternalDateMsgAttr: case imap.FetchInternalDate:
msg.InternalDate = time.Unix(m.Time, 0) 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 // 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. // on our part and we need to compute "real" size of decrypted data.
if m.Size <= 0 { if m.Size <= 0 {
@ -301,7 +302,7 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []str
} }
} }
msg.Size = uint32(m.Size) msg.Size = uint32(m.Size)
case imap.UidMsgAttr: case imap.FetchUid:
msg.Uid, err = storeMessage.UID() msg.Uid, err = storeMessage.UID()
if err != nil { if err != nil {
return nil, err return nil, err
@ -310,7 +311,7 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []str
s := item s := item
var section *imap.BodySectionName var section *imap.BodySectionName
if section, err = imap.NewBodySectionName(s); err != nil { if section, err = imap.ParseBodySectionName(s); err != nil {
err = nil // Ignore error err = nil // Ignore error
break 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. // 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. // Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
response, err = structure.GetSectionContent(bodyReader, section.Path) 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. // The MIME part specifier refers to the [MIME-IMB] header for this part.
fallthrough fallthrough
case section.Specifier == imap.HeaderSpecifier: case section.Specifier == imap.HeaderSpecifier:
header, err = structure.GetSectionHeader(section.Path) header, err = structure.GetSectionHeader(section.Path)
default: 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 return
} }
func (im *imapMailbox) customMessage(m *pmapi.Message, err error, attachBody bool) { const customMessageTemplate = `
// Assuming quoted-printable. <html>
origBody := strings.Replace(m.Body, "=", "=3D", -1) <head></head>
m.Body = "Content-Type: text/html\r\n" <body style="font-family: Arial,'Helvetica Neue',Helvetica,sans-serif; font-size: 14px;">
m.Body = "\n<html><head></head><body style=3D\"font-family: Arial,'Helvetica Neue',Helvetica,sans-serif; font-size: 14px; \">\n" <div style="color:#555; background-color:#cf9696; padding:20px; border-radius: 4px;">
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" <strong>Decryption error</strong><br/>
m.Body += err.Error() Decryption of this message's encrypted content failed.
m.Body += "\n</pre></div>\n" <pre>{{.Error}}</pre>
</div>
if attachBody { {{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" <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;">
m.Body += origBody <pre>{{.Body}}</pre>
m.Body += "\n</pre></div>\n" </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 = pmapi.ContentTypeHTML
m.MIMEType = "text/html" m.Body = b.String()
// NOTE: we need to set header in custom message header, so we check that is non-nil. // NOTE: we need to set header in custom message header, so we check that is non-nil.
if m.Header == nil { if m.Header == nil {
m.Header = make(mail.Header) m.Header = make(mail.Header)
} }
return
} }
func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err error) { 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) err = message.WriteBody(w, kr, m)
if err != nil { 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) _, _ = io.WriteString(w, m.Body)
err = nil 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) { func (im *imapMailbox) writeAttachmentBody(w io.Writer, m *pmapi.Message, att *pmapi.Attachment) (err error) {
// Retrieve encrypted attachment. // Retrieve encrypted attachment.
r, err := im.user.client.GetAttachment(att.ID) r, err := im.user.client().GetAttachment(att.ID)
if err != nil { if err != nil {
return return
} }
defer r.Close() //nolint[errcheck] 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 { if err = message.WriteAttachmentBody(w, kr, m, att, r); err != nil {
// Returning an error here makes certain mail clients behave badly, // Returning an error here makes certain mail clients behave badly,
// trying to retrieve the message again and again. // trying to retrieve the message again and again.
@ -646,12 +682,19 @@ func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodySt
return 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) errDecrypt := m.Decrypt(kr)
if errDecrypt != nil && errDecrypt != openpgperrors.ErrSignatureExpired { if errDecrypt != nil && errDecrypt != openpgperrors.ErrSignatureExpired {
errNoCache.add(errDecrypt) 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. // 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 return nil, nil, err
} else if err != nil { } else if err != nil {
errNoCache.add(err) 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) structure, msgBody, err = im.buildMessageInner(m, kr)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -677,7 +722,7 @@ func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodySt
return structure, msgBody, err 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) multipartType, err := im.setMessageContentType(m)
if err != nil { if err != nil {
return return

View File

@ -152,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. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() 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") 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.") log.Warn("Body and Text criteria not applied.")
} }
var apiIDs []string var apiIDs []string
if criteria.SeqSet != nil { if criteria.SeqNum != nil {
apiIDs, err = im.apiIDsFromSeqSet(false, criteria.SeqSet) apiIDs, err = im.apiIDsFromSeqSet(false, criteria.SeqNum)
} else { } else {
apiIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(1, 0) apiIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(1, 0)
} }
@ -170,20 +170,15 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
return nil, err return nil, err
} }
var apiIDsFromUID []string
if criteria.Uid != nil { if criteria.Uid != nil {
if apiIDs, err := im.apiIDsFromSeqSet(true, criteria.Uid); err == nil { apiIDsByUID, err := im.apiIDsFromSeqSet(true, criteria.Uid)
apiIDsFromUID = append(apiIDsFromUID, apiIDs...) if err != nil {
return nil, err
} }
apiIDs = arrayIntersection(apiIDs, apiIDsByUID)
} }
// Apply filters.
for _, apiID := range apiIDs { for _, apiID := range apiIDs {
// Filter on UIDs.
if len(apiIDsFromUID) > 0 && !isStringInList(apiIDsFromUID, apiID) {
continue
}
// Get message. // Get message.
storeMessage, err := im.storeMailbox.GetMessage(apiID) storeMessage, err := im.storeMailbox.GetMessage(apiID)
if err != nil { if err != nil {
@ -192,79 +187,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
} }
m := storeMessage.Message() m := storeMessage.Message()
// Filter addresses. // Filter by time.
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.
if !criteria.Before.IsZero() { if !criteria.Before.IsZero() {
if truncated := criteria.Before.Truncate(24 * time.Hour); m.Time > truncated.Unix() { if truncated := criteria.Before.Truncate(24 * time.Hour); m.Time > truncated.Unix() {
continue continue
@ -275,15 +198,8 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
continue continue
} }
} }
if !criteria.On.IsZero() { if !criteria.SentBefore.IsZero() || !criteria.SentSince.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 t, err := m.Header.Date(); err == nil && !t.IsZero() { if t, err := m.Header.Date(); err == nil && !t.IsZero() {
// Filter header date.
if !criteria.SentBefore.IsZero() { if !criteria.SentBefore.IsZero() {
if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() { if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() {
continue continue
@ -294,16 +210,81 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
continue 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 m.Size > 0 {
if criteria.Larger != 0 && m.Size <= int64(criteria.Larger) { if criteria.Larger != 0 && m.Size <= int64(criteria.Larger) {
continue continue
@ -337,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. // 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. // 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() { defer func() {
close(msgResponse) close(msgResponse)
if err != nil { if err != nil {
@ -452,13 +433,17 @@ func (im *imapMailbox) apiIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]string
return apiIDs, nil return apiIDs, nil
} }
func isAddressInList(addrs []*mail.Address, query string) bool { //nolint[deadcode] func arrayIntersection(a, b []string) (c []string) {
for _, addr := range addrs { m := make(map[string]bool)
if strings.Contains(addr.Address, query) || strings.Contains(addr.Name, query) { for _, item := range a {
return true 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 { func isStringInList(list []string, s string) bool {
@ -478,12 +463,3 @@ func addressMatch(addresses []*mail.Address, criteria string) bool {
} }
return false 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
}

View File

@ -71,29 +71,25 @@ func (m *imapRootMailbox) Info() (info *imap.MailboxInfo, err error) {
return return
} }
func (m *imapRootMailbox) Status(items []string) (status *imap.MailboxStatus, err error) { func (m *imapRootMailbox) Status(_ []imap.StatusItem) (*imap.MailboxStatus, error) {
status = &imap.MailboxStatus{} status := &imap.MailboxStatus{}
if m.isFolder { if m.isFolder {
status.Name = store.UserFoldersMailboxName status.Name = store.UserFoldersMailboxName
} else { } else {
status.Name = store.UserLabelsMailboxName status.Name = store.UserLabelsMailboxName
} }
return return status, nil
} }
func (m *imapRootMailbox) Subscribe() error { func (m *imapRootMailbox) SetSubscribed(_ bool) error {
return errors.New("cannot subscribe to Labels or Folders mailboxes") return errors.New("cannot subscribe or unsubsribe to Labels or Folders mailboxes")
}
func (m *imapRootMailbox) Unsubscribe() error {
return errors.New("cannot unsubscribe from Labels or Folders mailboxes")
} }
func (m *imapRootMailbox) Check() error { func (m *imapRootMailbox) Check() error {
return nil 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) close(ch)
return nil return nil
} }

View File

@ -21,6 +21,7 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
"net"
"strings" "strings"
"time" "time"
@ -44,6 +45,8 @@ import (
type imapServer struct { type imapServer struct {
server *imapserver.Server server *imapserver.Server
eventListener listener.Listener eventListener listener.Listener
debugClient bool
debugServer bool
} }
// NewIMAPServer constructs a new IMAP server configured with the given options. // NewIMAPServer constructs a new IMAP server configured with the given options.
@ -54,17 +57,7 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
s.AllowInsecureAuth = true s.AllowInsecureAuth = true
s.ErrorLog = newServerErrorLogger("server-imap") s.ErrorLog = newServerErrorLogger("server-imap")
s.AutoLogout = 30 * time.Minute s.AutoLogout = 30 * time.Minute
s.UpgradeError = imapBackend.upgradeError
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{ serverID := imapid.ID{
imapid.FieldName: "ProtonMail", imapid.FieldName: "ProtonMail",
@ -76,14 +69,27 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
conn.Server().ForEachConn(func(candidate imapserver.Conn) { conn.Server().ForEachConn(func(candidate imapserver.Conn) {
if id, ok := candidate.(imapid.Conn); ok { if id, ok := candidate.(imapid.Conn); ok {
if conn.Context() == candidate.Context() { if conn.Context() == candidate.Context() {
imapBackend.setLastMailClient(id.ID()) // ID is not available right at the beginning of the connection.
return // 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())
break
}
if time.Since(start) > 10*time.Second {
break
}
time.Sleep(100 * time.Millisecond)
}
}()
} }
} }
}) })
return sasl.NewLoginServer(func(address, password string) error { 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 { if err != nil {
return err return err
} }
@ -109,6 +115,8 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
return &imapServer{ return &imapServer{
server: s, server: s,
eventListener: eventListener, eventListener: eventListener,
debugClient: debugClient,
debugServer: debugServer,
} }
} }
@ -117,7 +125,17 @@ func (s *imapServer) ListenAndServe() {
go s.monitorDisconnectedUsers() go s.monitorDisconnectedUsers()
log.Info("IMAP server listening at ", s.server.Addr) 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 { if err != nil {
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error()) s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
log.Error("IMAP failed: ", err) log.Error("IMAP failed: ", err)
@ -150,20 +168,38 @@ func (s *imapServer) monitorDisconnectedUsers() {
} }
} }
// logWithFields is used for debuging with additional field. // debugListener sets debug loggers on server containing fields with local
type logWithFields struct { // and remote addresses right after new connection is accepted.
log *logrus.Entry type debugListener struct {
fields logrus.Fields net.Listener
server *imapServer
} }
func (lf *logWithFields) Writer() io.Writer { func (dl *debugListener) Accept() (net.Conn, error) {
w := lf.log.WithFields(lf.fields).WriterLevel(logrus.DebugLevel) conn, err := dl.Listener.Accept()
lf.fields = logrus.Fields{}
return w
}
func (lf *logWithFields) SetField(key, value string) { if err == nil && (dl.server.debugServer || dl.server.debugClient) {
lf.fields[key] = value 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. // serverErrorLogger implements go-imap/logger interface.
@ -175,17 +211,12 @@ func newServerErrorLogger(tag string) *serverErrorLogger {
return &serverErrorLogger{tag} return &serverErrorLogger{tag}
} }
func (s *serverErrorLogger) CheckErrorForReport(serverErr string) {
}
func (s *serverErrorLogger) Printf(format string, args ...interface{}) { func (s *serverErrorLogger) Printf(format string, args ...interface{}) {
err := fmt.Sprintf(format, args...) err := fmt.Sprintf(format, args...)
s.CheckErrorForReport(err)
log.WithField("pkg", s.tag).Error(err) log.WithField("pkg", s.tag).Error(err)
} }
func (s *serverErrorLogger) Println(args ...interface{}) { func (s *serverErrorLogger) Println(args ...interface{}) {
err := fmt.Sprintln(args...) err := fmt.Sprintln(args...)
s.CheckErrorForReport(err)
log.WithField("pkg", s.tag).Error(err) log.WithField("pkg", s.tag).Error(err)
} }

View File

@ -21,7 +21,7 @@ import (
"io" "io"
"net/mail" "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/imap/uidplus"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -35,7 +35,7 @@ type storeUserProvider interface {
GetAddress(addressID string) (storeAddressProvider, error) GetAddress(addressID string) (storeAddressProvider, error)
CreateDraft( CreateDraft(
kr *pmcrypto.KeyRing, kr *crypto.KeyRing,
message *pmapi.Message, message *pmapi.Message,
attachmentReaders []io.Reader, attachmentReaders []io.Reader,
attachedPublicKey, attachedPublicKey,
@ -68,7 +68,7 @@ type storeMailboxProvider interface {
GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error) GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error)
GetLatestAPIID() (string, error) GetLatestAPIID() (string, error)
GetNextUID() (uint32, error) GetNextUID() (uint32, error)
GetCounts() (dbTotal, dbUnread uint, err error) GetCounts() (dbTotal, dbUnread, dbUnreadSeqNum uint, err error)
GetUIDList(apiIDs []string) *uidplus.OrderedSeq GetUIDList(apiIDs []string) *uidplus.OrderedSeq
GetUIDByHeader(header *mail.Header) uint32 GetUIDByHeader(header *mail.Header) uint32
GetDelimiter() string GetDelimiter() string

View File

@ -141,7 +141,7 @@ func (ext *extension) Capabilities(c server.Conn) []string {
} }
func (ext *extension) Command(name string) server.HandlerFactory { func (ext *extension) Command(name string) server.HandlerFactory {
if name == imap.Expunge { if name == "EXPUNGE" {
return func() server.Handler { return func() server.Handler {
return &UIDExpunge{} return &UIDExpunge{}
} }
@ -165,7 +165,7 @@ func getStatusResponseCopy(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq)
} }
return &imap.StatusResp{ return &imap.StatusResp{
Type: imap.StatusOk, Type: imap.StatusRespOk,
Info: info, Info: info,
} }
} }
@ -187,7 +187,7 @@ func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.St
} }
return &imap.StatusResp{ return &imap.StatusResp{
Type: imap.StatusOk, Type: imap.StatusRespOk,
Info: info, Info: info,
} }
} }

View File

@ -21,7 +21,7 @@ import (
"errors" "errors"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapquota "github.com/emersion/go-imap-quota" imapquota "github.com/emersion/go-imap-quota"
goIMAPBackend "github.com/emersion/go-imap/backend" goIMAPBackend "github.com/emersion/go-imap/backend"
) )
@ -34,7 +34,6 @@ type imapUser struct {
panicHandler panicHandler panicHandler panicHandler
backend *imapBackend backend *imapBackend
user bridgeUser user bridgeUser
client bridge.PMAPIProvider
storeUser storeUserProvider storeUser storeUserProvider
storeAddress storeAddressProvider storeAddress storeAddressProvider
@ -42,6 +41,11 @@ type imapUser struct {
currentAddressLowercase string 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. // newIMAPUser returns struct implementing go-imap/user interface.
func newIMAPUser( func newIMAPUser(
panicHandler panicHandler, panicHandler panicHandler,
@ -62,13 +66,10 @@ func newIMAPUser(
return nil, err return nil, err
} }
client := user.GetTemporaryPMAPIClient()
return &imapUser{ return &imapUser{
panicHandler: panicHandler, panicHandler: panicHandler,
backend: backend, backend: backend,
user: user, user: user,
client: client,
storeUser: storeUser, storeUser: storeUser,
storeAddress: storeAddress, storeAddress: storeAddress,

View File

@ -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)
}
}

View File

@ -19,6 +19,8 @@ package smtp
import ( import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
type bridger interface { type bridger interface {
@ -29,7 +31,7 @@ type bridgeUser interface {
CheckBridgeLogin(password string) error CheckBridgeLogin(password string) error
IsCombinedAddressMode() bool IsCombinedAddressMode() bool
GetAddressID(address string) (string, error) GetAddressID(address string) (string, error)
GetTemporaryPMAPIClient() bridge.PMAPIProvider GetTemporaryPMAPIClient() pmapi.Client
GetStore() storeUserProvider GetStore() storeUserProvider
} }
@ -53,10 +55,10 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
} }
type bridgeUserWrap struct { type bridgeUserWrap struct {
*bridge.User *users.User
} }
func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap { func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
return &bridgeUserWrap{User: bridgeUser} return &bridgeUserWrap{User: bridgeUser}
} }

View 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
}

View File

@ -20,7 +20,7 @@ package smtp
import ( import (
"errors" "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/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/algo" "github.com/ProtonMail/proton-bridge/pkg/algo"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
@ -37,7 +37,7 @@ type SendingInfo struct {
Sign bool Sign bool
Scheme int Scheme int
MIMEType string MIMEType string
PublicKey *pmcrypto.KeyRing PublicKey *crypto.KeyRing
} }
func generateSendingInfo( func generateSendingInfo(
@ -46,10 +46,10 @@ func generateSendingInfo(
isInternal bool, isInternal bool,
composeMode string, composeMode string,
apiKeys, apiKeys,
contactKeys []*pmcrypto.KeyRing, contactKeys []*crypto.KeyRing,
settingsSign bool, settingsSign bool,
settingsPgpScheme int) (sendingInfo SendingInfo, err error) { settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
contactKeys, err = pmcrypto.FilterExpiredKeys(contactKeys) contactKeys, err = crypto.FilterExpiredKeys(contactKeys)
if err != nil { if err != nil {
return return
} }
@ -72,7 +72,7 @@ func generateInternalSendingInfo(
contactMeta *ContactMetadata, contactMeta *ContactMetadata,
composeMode string, composeMode string,
apiKeys, apiKeys,
contactKeys []*pmcrypto.KeyRing, contactKeys []*crypto.KeyRing,
settingsSign bool, //nolint[unparam] settingsSign bool, //nolint[unparam]
settingsPgpScheme int) (sendingInfo SendingInfo, err error) { //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. // If sending internally, there should always be a public key; if not, there's an error.
@ -125,7 +125,7 @@ func generateExternalSendingInfo(
contactMeta *ContactMetadata, contactMeta *ContactMetadata,
composeMode string, composeMode string,
apiKeys, apiKeys,
contactKeys []*pmcrypto.KeyRing, contactKeys []*crypto.KeyRing,
settingsSign bool, settingsSign bool,
settingsPgpScheme int) (sendingInfo SendingInfo, err error) { settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
// The default settings, unless overridden by presence of a saved contact. // 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 // checkContactKeysAgainstAPI keeps only those contact keys which are up to date and have
// an ID that matches an API key's ID. // 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 { keyIDsAreEqual := func(a, b interface{}) bool {
aKey, bKey := a.(*pmcrypto.KeyRing), b.(*pmcrypto.KeyRing) aKey, bKey := a.(*crypto.KeyRing), b.(*crypto.KeyRing)
return aKey.GetEntities()[0].PrimaryKey.KeyId == bKey.GetEntities()[0].PrimaryKey.KeyId
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) { for _, v := range algo.SetIntersection(contactKeys, apiKeys, keyIDsAreEqual) {
filteredKeys = append(filteredKeys, v.(*pmcrypto.KeyRing)) filteredKeys = append(filteredKeys, v.(*crypto.KeyRing))
} }
return return

View File

@ -18,36 +18,36 @@
package smtp package smtp
import ( import (
"strings"
"testing" "testing"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "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/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type mocks struct { type mocks struct {
t *testing.T t *testing.T
eventListener *bridge.MockListener eventListener *users.MockListener
} }
func initMocks(t *testing.T) mocks { func initMocks(t *testing.T) mocks {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
return mocks{ return mocks{
t: t, t: t,
eventListener: bridge.NewMockListener(mockCtrl), eventListener: users.NewMockListener(mockCtrl),
} }
} }
type args struct { type args struct {
eventListener listener.Listener eventListener listener.Listener
contactMeta *ContactMetadata contactMeta *ContactMetadata
apiKeys []*pmcrypto.KeyRing apiKeys []*crypto.KeyRing
contactKeys []*pmcrypto.KeyRing contactKeys []*crypto.KeyRing
composeMode string composeMode string
settingsPgpScheme int settingsPgpScheme int
settingsSign bool settingsSign bool
@ -68,18 +68,61 @@ func (tt *testData) runTest(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
assert.NoError(t, err) 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) { func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
m := initMocks(t) m := initMocks(t)
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) pubKey := keyRingFromKey(testPublicKey)
if err != nil {
panic(err)
}
tests := []testData{ tests := []testData{
{ {
@ -88,8 +131,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -107,8 +150,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -126,8 +169,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -145,8 +188,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -164,8 +207,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: false, settingsSign: false,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -183,8 +226,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
eventListener: m.eventListener, eventListener: m.eventListener,
contactMeta: nil, contactMeta: nil,
isInternal: true, isInternal: true,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
}, },
wantSendingInfo: SendingInfo{}, wantSendingInfo: SendingInfo{},
wantErr: true, wantErr: true,
@ -195,8 +238,8 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
contactMeta: nil, contactMeta: nil,
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -217,20 +260,11 @@ func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
func TestGenerateSendingInfo_Contact_Internal(t *testing.T) { func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
m := initMocks(t) m := initMocks(t)
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) pubKey := keyRingFromKey(testPublicKey)
if err != nil {
panic(err)
}
preferredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) preferredPubKey := keyRingFromKey(testPublicKey)
if err != nil {
panic(err)
}
differentPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testDifferentPublicKey)) differentPubKey := keyRingFromKey(testDifferentPublicKey)
if err != nil {
panic(err)
}
m.eventListener.EXPECT().Emit(events.NoActiveKeyForRecipientEvent, "badkey@email.com") 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"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -260,8 +294,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -279,8 +313,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{preferredPubKey}, apiKeys: []*crypto.KeyRing{preferredPubKey},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, 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"}, contactMeta: &ContactMetadata{Email: "badkey@email.com", Encrypt: true, Scheme: "pgp-mime"},
isInternal: true, isInternal: true,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{differentPubKey}, contactKeys: []*crypto.KeyRing{differentPubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -313,8 +347,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -332,8 +366,8 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey}, apiKeys: []*crypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{differentPubKey}, contactKeys: []*crypto.KeyRing{differentPubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -352,15 +386,9 @@ func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
} }
func TestGenerateSendingInfo_Contact_External(t *testing.T) { func TestGenerateSendingInfo_Contact_External(t *testing.T) {
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey)) pubKey := keyRingFromKey(testPublicKey)
if err != nil {
panic(err)
}
expiredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testExpiredPublicKey)) expiredPubKey := keyRingFromKey(testExpiredPublicKey)
if err != nil {
panic(err)
}
tests := []testData{ tests := []testData{
{ {
@ -369,8 +397,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -388,8 +416,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{expiredPubKey}, contactKeys: []*crypto.KeyRing{expiredPubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -407,8 +435,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -426,8 +454,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-inline"}, contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-inline"},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -445,8 +473,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{Encrypt: true}, contactMeta: &ContactMetadata{Encrypt: true},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey}, contactKeys: []*crypto.KeyRing{pubKey},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage, settingsPgpScheme: pmapi.PGPMIMEPackage,
}, },
@ -464,8 +492,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -483,8 +511,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{MIMEType: pmapi.ContentTypePlainText}, contactMeta: &ContactMetadata{MIMEType: pmapi.ContentTypePlainText},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },
@ -502,8 +530,8 @@ func TestGenerateSendingInfo_Contact_External(t *testing.T) {
contactMeta: &ContactMetadata{SignMissing: true}, contactMeta: &ContactMetadata{SignMissing: true},
isInternal: false, isInternal: false,
composeMode: pmapi.ContentTypeHTML, composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{}, apiKeys: []*crypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{}, contactKeys: []*crypto.KeyRing{},
settingsSign: true, settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage, settingsPgpScheme: pmapi.PGPInlinePackage,
}, },

View File

@ -18,8 +18,8 @@
// Package smtp provides SMTP server of the Bridge. // Package smtp provides SMTP server of the Bridge.
package smtp package smtp
import "github.com/ProtonMail/proton-bridge/pkg/config" import "github.com/sirupsen/logrus"
var ( var (
log = config.GetLogEntry("smtp") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "smtp") //nolint[gochecknoglobals]
) )

View File

@ -20,13 +20,13 @@ package smtp
import ( import (
"io" "io"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
type storeUserProvider interface { type storeUserProvider interface {
CreateDraft( CreateDraft(
kr *pmcrypto.KeyRing, kr *crypto.KeyRing,
message *pmapi.Message, message *pmapi.Message,
attachmentReaders []io.Reader, attachmentReaders []io.Reader,
attachedPublicKey, attachedPublicKey,

View File

@ -20,7 +20,6 @@
package smtp package smtp
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
@ -32,8 +31,7 @@ import (
"strings" "strings"
"time" "time"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/message"
@ -47,7 +45,6 @@ type smtpUser struct {
eventListener listener.Listener eventListener listener.Listener
backend *smtpBackend backend *smtpBackend
user bridgeUser user bridgeUser
client bridge.PMAPIProvider
storeUser storeUserProvider storeUser storeUserProvider
addressID string addressID string
} }
@ -60,9 +57,6 @@ func newSMTPUser(
user bridgeUser, user bridgeUser,
addressID string, addressID string,
) (goSMTPBackend.User, error) { ) (goSMTPBackend.User, error) {
// Using client directly is deprecated. Code should be moved to store.
client := user.GetTemporaryPMAPIClient()
storeUser := user.GetStore() storeUser := user.GetStore()
if storeUser == nil { if storeUser == nil {
return nil, errors.New("user database is not initialized") return nil, errors.New("user database is not initialized")
@ -73,37 +67,51 @@ func newSMTPUser(
eventListener: eventListener, eventListener: eventListener,
backend: smtpBackend, backend: smtpBackend,
user: user, user: user,
client: client,
storeUser: storeUser, storeUser: storeUser,
addressID: addressID, addressID: addressID,
}, nil }, 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. // 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] 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. // Called from go-smtp in goroutines - we need to handle panics for each function.
defer su.panicHandler.HandlePanic() defer su.panicHandler.HandlePanic()
mailSettings, err := su.client.GetMailSettings() mailSettings, err := su.client().GetMailSettings()
if err != nil { if err != nil {
return err return err
} }
var addr *pmapi.Address = su.client.Addresses().ByEmail(from) var addr *pmapi.Address = su.client().Addresses().ByEmail(from)
if addr == nil { if addr == nil {
err = errors.New("backend: invalid email address: not owned by user") err = errors.New("backend: invalid email address: not owned by user")
return return
} }
kr := addr.KeyRing()
kr, err := su.client().KeyRingForAddressID(addr.ID)
if err != nil {
return
}
var attachedPublicKey string var attachedPublicKey string
var attachedPublicKeyName string var attachedPublicKeyName string
if mailSettings.AttachPublicKey > 0 { if mailSettings.AttachPublicKey > 0 {
attachedPublicKey, err = kr.GetArmoredPublicKey() firstKey, err := kr.GetKey(0)
if err != nil { if err != nil {
return err 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) 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 // but it's better than sending the message many times. If the message was sent, we simply return
// nil to indicate it's OK. // nil to indicate it's OK.
sendRecorderMessageHash := su.backend.sendRecorder.getMessageHash(message) 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 { if isSending {
log.Debug("Message is in send queue, waiting") log.Debug("Message is in send queue, waiting")
time.Sleep(60 * time.Second) 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 { if isSending {
log.Debug("Message is still in send queue, returning error") 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 // can lead to sending the wrong message. Also clients do not necessarily
// delete the old draft. // delete the old draft.
if draftID != "" { 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") log.WithError(err).WithField("draftID", draftID).Warn("Original draft cannot be deleted")
} }
} }
atts = append(atts, message.Attachments...) atts = append(atts, message.Attachments...)
// Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys. // 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) attkeysEncoded := make(map[string]pmapi.AlgoKey)
for _, att := range atts { for _, att := range atts {
@ -204,28 +212,32 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
// PMEL 3. // PMEL 3.
composeMode := message.MIMEType composeMode := message.MIMEType
var plainKey, htmlKey, mimeKey *pmcrypto.SymmetricKey var plainKey, htmlKey, mimeKey *crypto.SessionKey
var plainData, htmlData, mimeData []byte var plainData, htmlData, mimeData []byte
containsUnencryptedRecipients := false containsUnencryptedRecipients := false
for _, email := range to { for _, email := range to {
if !looksLikeEmail(email) {
return errors.New(`"` + email + `" is not a valid recipient.`)
}
// PMEL 1. // PMEL 1.
contactEmails, err := su.client.GetContactEmailByEmail(email, 0, 1000) contactEmails, err := su.client().GetContactEmailByEmail(email, 0, 1000)
if err != nil { if err != nil {
return err return err
} }
var contactMeta *ContactMetadata var contactMeta *ContactMetadata
var contactKeys []*pmcrypto.KeyRing var contactKeyRings []*crypto.KeyRing
for _, contactEmail := range contactEmails { for _, contactEmail := range contactEmails {
if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_ if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_
continue continue
} }
contact, err := su.client.GetContactByID(contactEmail.ContactID) contact, err := su.client().GetContactByID(contactEmail.ContactID)
if err != nil { if err != nil {
return err return err
} }
decryptedCards, err := su.client.DecryptAndVerifyCards(contact.Cards) decryptedCards, err := su.client().DecryptAndVerifyCards(contact.Cards)
if err != nil { if err != nil {
return err return err
} }
@ -233,34 +245,47 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
if err != nil { if err != nil {
return err return err
} }
contactKeyRing, err := crypto.NewKeyRing(nil)
if err != nil {
return err
}
for _, contactRawKey := range contactMeta.Keys { for _, contactRawKey := range contactMeta.Keys {
contactKey, err := pmcrypto.ReadKeyRing(bytes.NewBufferString(contactRawKey)) contactKey, err := crypto.NewKey([]byte(contactRawKey))
if err != nil { if err != nil {
return err return err
} }
contactKeys = append(contactKeys, contactKey) 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 break // We take the first hit where Defaults == 0, see "How to find the right contact" of PMEL
} }
// PMEL 4. // PMEL 4.
apiRawKeyList, isInternal, err := su.client.GetPublicKeysForEmail(email) apiRawKeyList, isInternal, err := su.client().GetPublicKeysForEmail(email)
if err != nil { if err != nil {
err = fmt.Errorf("backend: cannot get recipients' public keys: %v", err) err = fmt.Errorf("backend: cannot get recipients' public keys: %v", err)
return err return err
} }
var apiKeys []*pmcrypto.KeyRing var apiKeyRings []*crypto.KeyRing
for _, apiRawKey := range apiRawKeyList { for _, apiRawKey := range apiRawKeyList {
var kr *pmcrypto.KeyRing key, err := crypto.NewKeyFromArmored(apiRawKey.PublicKey)
if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(apiRawKey.PublicKey)); err != nil { if err != nil {
return err return err
} }
apiKeys = append(apiKeys, kr)
kr, err := crypto.NewKeyRing(key)
if err != nil {
return err
}
apiKeyRings = append(apiKeyRings, kr)
} }
sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme) sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeyRings, contactKeyRings, settingsSign, settingsPgpScheme)
if !sendingInfo.Encrypt { if !sendingInfo.Encrypt {
containsUnencryptedRecipients = true containsUnencryptedRecipients = true
} }
@ -281,7 +306,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
} }
} }
if sendingInfo.Scheme == pmapi.PGPMIMEPackage { 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 { if err != nil {
return err 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")) return errors.New("error decoding subject message " + message.Header.Get("Subject"))
} }
if !su.continueSendingUnencryptedMail(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") 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") references := m.Header.Get("References")
newReferences := []string{} newReferences := []string{}
for _, reference := range strings.Fields(references) { for _, reference := range strings.Fields(references) {
if !strings.Contains(reference, "@protonmail.internalid") { if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) {
newReferences = append(newReferences, reference) newReferences = append(newReferences, reference)
} else { // internalid is the parentID. } else { // internalid is the parentID.
idMatch := regexp.MustCompile("[a-zA-Z0-9-_=]*@protonmail.internalid").FindString(reference) idMatch := regexp.MustCompile(pmapi.InternalReferenceFormat).FindStringSubmatch(reference)
if idMatch != "" { if len(idMatch) > 0 {
lastID := idMatch[0 : len(idMatch)-len("@protonmail.internalid")] lastID := strings.TrimSuffix(strings.Trim(idMatch[0], "<>"), "@protonmail.internalid")
filter := &pmapi.MessagesFilter{ID: []string{lastID}} filter := &pmapi.MessagesFilter{ID: []string{lastID}}
if su.addressID != "" { if su.addressID != "" {
filter.AddressID = su.addressID filter.AddressID = su.addressID
} }
metadata, _, _ := su.client.ListMessages(filter) metadata, _, _ := su.client().ListMessages(filter)
for _, m := range metadata { for _, m := range metadata {
if isDraft(m) { if m.IsDraft() {
draftID = m.ID draftID = m.ID
} else { } else {
parentID = m.ID parentID = m.ID
@ -401,7 +426,7 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
if su.addressID != "" { if su.addressID != "" {
filter.AddressID = 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 // 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. // be sure which message should be parent. Better to not choose any.
if len(metadata) == 1 { if len(metadata) == 1 {
@ -412,15 +437,6 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
return 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) { func (su *smtpUser) handleSenderAndRecipients(m *pmapi.Message, addr *pmapi.Address, from string, to []string) (err error) {
from = pmapi.ConstructAddress(from, addr.Email) from = pmapi.ConstructAddress(from, addr.Email)

View File

@ -19,15 +19,27 @@ package smtp
import ( import (
"encoding/base64" "encoding/base64"
"regexp"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "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( func createPackets(
pubkey *pmcrypto.KeyRing, pubkey *crypto.KeyRing,
bodyKey *pmcrypto.SymmetricKey, bodyKey *crypto.SessionKey,
attkeys map[string]*pmcrypto.SymmetricKey, attkeys map[string]*crypto.SessionKey,
) (bodyPacket string, attachmentPackets map[string]string, err error) { ) (bodyPacket string, attachmentPackets map[string]string, err error) {
// Encrypt message body keys. // Encrypt message body keys.
packetBytes, err := pubkey.EncryptSessionKey(bodyKey) packetBytes, err := pubkey.EncryptSessionKey(bodyKey)
@ -49,24 +61,33 @@ func createPackets(
} }
func encryptSymmetric( func encryptSymmetric(
kr *pmcrypto.KeyRing, kr *crypto.KeyRing,
textToEncrypt string, textToEncrypt string,
canonicalizeText bool, // nolint[unparam] 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). // 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 { if err != nil {
return return
} }
pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr)
if err != nil {
return
}
pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0) pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0)
if err != nil { if err != nil {
return return
} }
key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket()) key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket())
if err != nil { if err != nil {
return return
} }
symEncryptedData = pgpSplitMessage.GetBinaryDataPacket() symEncryptedData = pgpSplitMessage.GetBinaryDataPacket()
return return
} }
@ -75,7 +96,7 @@ func buildPackage(
sharedScheme int, sharedScheme int,
mimeType string, mimeType string,
bodyData []byte, bodyData []byte,
bodyKey *pmcrypto.SymmetricKey, bodyKey *crypto.SessionKey,
attKeys map[string]pmapi.AlgoKey, attKeys map[string]pmapi.AlgoKey,
) (pkg *pmapi.MessagePackage) { ) (pkg *pmapi.MessagePackage) {
if len(addressMap) == 0 { if len(addressMap) == 0 {

View File

@ -109,5 +109,9 @@ func (storeAddress *Address) AddressID() string {
// APIAddress returns the `pmapi.Address` struct. // APIAddress returns the `pmapi.Address` struct.
func (storeAddress *Address) APIAddress() *pmapi.Address { 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()
} }

View File

@ -78,6 +78,7 @@ func (storeAddress *Address) createOrUpdateMailboxEvent(label *pmapi.Label) erro
return err return err
} }
storeAddress.mailboxes[label.ID] = mailbox storeAddress.mailboxes[label.ID] = mailbox
mailbox.store.imapMailboxCreated(storeAddress.address, mailbox.labelName)
} else { } else {
mailbox.labelName = prefix + label.Name mailbox.labelName = prefix + label.Name
mailbox.color = label.Color mailbox.color = label.Color

View File

@ -29,7 +29,7 @@ import (
// SetIMAPUpdateChannel sets the channel on which imap update messages will be sent. This should be the channel // 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. // 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") store.log.Debug("Listening for IMAP updates")
if store.imapUpdates = updates; store.imapUpdates == nil { 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) { func (store *Store) imapNotice(address, notice string) {
update := new(imapBackend.StatusUpdate) update := new(imapBackend.StatusUpdate)
update.Username = address update.Update = imapBackend.NewUpdate(address, "")
update.StatusResp = &imap.StatusResp{ update.StatusResp = &imap.StatusResp{
Type: imap.StatusOk, Type: imap.StatusRespOk,
Code: imap.CodeAlert, Code: imap.CodeAlert,
Info: notice, Info: notice,
} }
@ -57,9 +57,8 @@ func (store *Store) imapUpdateMessage(address, mailboxName string, uid, sequence
"flags": message.GetFlags(msg), "flags": message.GetFlags(msg),
}).Trace("IDLE update") }).Trace("IDLE update")
update := new(imapBackend.MessageUpdate) update := new(imapBackend.MessageUpdate)
update.Username = address update.Update = imapBackend.NewUpdate(address, mailboxName)
update.Mailbox = mailboxName update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
update.Message = imap.NewMessage(sequenceNumber, []string{imap.FlagsMsgAttr, imap.UidMsgAttr})
update.Message.Flags = message.GetFlags(msg) update.Message.Flags = message.GetFlags(msg)
update.Message.Uid = uid update.Message.Uid = uid
store.imapSendUpdate(update) store.imapSendUpdate(update)
@ -72,29 +71,44 @@ func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumbe
"seqNum": sequenceNumber, "seqNum": sequenceNumber,
}).Trace("IDLE delete") }).Trace("IDLE delete")
update := new(imapBackend.ExpungeUpdate) update := new(imapBackend.ExpungeUpdate)
update.Username = address update.Update = imapBackend.NewUpdate(address, mailboxName)
update.Mailbox = mailboxName
update.SeqNum = sequenceNumber update.SeqNum = sequenceNumber
store.imapSendUpdate(update) 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{ store.log.WithFields(logrus.Fields{
"address": address, "address": address,
"mailbox": mailboxName, "mailbox": mailboxName,
"total": total, }).Trace("IDLE mailbox info")
"unread": unread, update := new(imapBackend.MailboxInfoUpdate)
}).Trace("IDLE status") update.Update = imapBackend.NewUpdate(address, "")
update := new(imapBackend.MailboxUpdate) update.MailboxInfo = &imap.MailboxInfo{
update.Username = address Attributes: []string{imap.NoInferiorsAttr},
update.Mailbox = mailboxName Delimiter: PathDelimiter,
update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []string{imap.MailboxMessages, imap.MailboxUnseen}) Name: mailboxName,
update.MailboxStatus.Messages = uint32(total) }
update.MailboxStatus.Unseen = uint32(unread)
store.imapSendUpdate(update) store.imapSendUpdate(update)
} }
func (store *Store) imapSendUpdate(update interface{}) { 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.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 imapBackend.Update) {
if store.imapUpdates == nil { if store.imapUpdates == nil {
store.log.Trace("IMAP IDLE unavailable") store.log.Trace("IMAP IDLE unavailable")
return return

View File

@ -29,7 +29,7 @@ func TestCreateOrUpdateMessageIMAPUpdates(t *testing.T) {
m, clear := initMocks(t) m, clear := initMocks(t)
defer clear() defer clear()
updates := make(chan interface{}) updates := make(chan imapBackend.Update)
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.store.SetIMAPUpdateChannel(updates) m.store.SetIMAPUpdateChannel(updates)
@ -49,7 +49,7 @@ func TestCreateOrUpdateMessageIMAPUpdatesBulkUpdate(t *testing.T) {
m, clear := initMocks(t) m, clear := initMocks(t)
defer clear() defer clear()
updates := make(chan interface{}) updates := make(chan imapBackend.Update)
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.store.SetIMAPUpdateChannel(updates) 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, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", 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) m.store.SetIMAPUpdateChannel(updates)
go checkIMAPUpdates(t, updates, []func(interface{}) bool{ go checkIMAPUpdates(t, updates, []func(interface{}) bool{
checkMessageDelete(addr1, "All Mail", 2), checkMessageDelete(addr1, "All Mail", 2),
@ -87,7 +87,7 @@ func TestDeleteMessageIMAPUpdate(t *testing.T) {
close(updates) 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 idx := 0
for update := range updates { for update := range updates {
if idx >= len(checkFunctions) { if idx >= len(checkFunctions) {
@ -105,8 +105,8 @@ func checkMessageUpdate(username, mailbox string, seqNum, uid int) func(interfac
return func(update interface{}) bool { return func(update interface{}) bool {
switch u := update.(type) { switch u := update.(type) {
case *imapBackend.MessageUpdate: case *imapBackend.MessageUpdate:
return (u.Update.Username == username && return (u.Update.Username() == username &&
u.Update.Mailbox == mailbox && u.Update.Mailbox() == mailbox &&
u.Message.SeqNum == uint32(seqNum) && u.Message.SeqNum == uint32(seqNum) &&
u.Message.Uid == uint32(uid)) u.Message.Uid == uint32(uid))
default: default:
@ -119,8 +119,8 @@ func checkMessageDelete(username, mailbox string, seqNum int) func(interface{})
return func(update interface{}) bool { return func(update interface{}) bool {
switch u := update.(type) { switch u := update.(type) {
case *imapBackend.ExpungeUpdate: case *imapBackend.ExpungeUpdate:
return (u.Update.Username == username && return (u.Update.Username() == username &&
u.Update.Mailbox == mailbox && u.Update.Mailbox() == mailbox &&
u.SeqNum == uint32(seqNum)) u.SeqNum == uint32(seqNum))
default: default:
return false return false

View File

@ -117,17 +117,16 @@ func TestCooldownIncreaseAndReset(t *testing.T) {
func TestCooldownNotSooner(t *testing.T) { func TestCooldownNotSooner(t *testing.T) {
var testCooldown cooldown var testCooldown cooldown
waitTime := 100 * time.Millisecond waitTime := 100 * time.Millisecond
retries := int64(10)
retryWait := time.Duration(waitTime.Milliseconds()/retries) * time.Millisecond
testCooldown.setWaitTimes(waitTime) testCooldown.setWaitTimes(waitTime)
// first time it should never be too soon // First time it should never be too soon.
assert.False(t, testCooldown.isTooSoon()) assert.False(t, testCooldown.isTooSoon())
// these retries should be too soon
for i := retries; i > 0; i-- { // Only half of given wait time should be too soon.
assert.True(t, testCooldown.isTooSoon()) time.Sleep(waitTime / 2)
time.Sleep(retryWait) assert.True(t, testCooldown.isTooSoon())
}
// after given wait time it shouldn't be soon anymore // After given wait time it shouldn't be soon anymore.
time.Sleep(waitTime / 2)
assert.False(t, testCooldown.isTooSoon()) assert.False(t, testCooldown.isTooSoon())
} }

View File

@ -34,6 +34,7 @@ const pollIntervalSpread = 5 * time.Second
type eventLoop struct { type eventLoop struct {
cache *Cache cache *Cache
currentEventID string currentEventID string
currentEvent *pmapi.Event
pollCh chan chan struct{} pollCh chan chan struct{}
stopCh chan struct{} stopCh chan struct{}
notifyStopCh chan struct{} notifyStopCh chan struct{}
@ -44,13 +45,12 @@ type eventLoop struct {
log *logrus.Entry log *logrus.Entry
store *Store store *Store
apiClient PMAPIProvider user BridgeUser
user BridgeUser events listener.Listener
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 := log.WithField("userID", user.ID())
eventLog.Trace("Creating new event loop") eventLog.Trace("Creating new event loop")
@ -62,10 +62,9 @@ func newEventLoop(cache *Cache, store *Store, api PMAPIProvider, user BridgeUser
log: eventLog, log: eventLog,
store: store, store: store,
apiClient: api, user: user,
user: user, events: events,
events: events,
} }
} }
@ -73,10 +72,14 @@ func (loop *eventLoop) IsRunning() bool {
return loop.isRunning return loop.isRunning
} }
func (loop *eventLoop) client() pmapi.Client {
return loop.store.client()
}
func (loop *eventLoop) setFirstEventID() (err error) { func (loop *eventLoop) setFirstEventID() (err error) {
loop.log.Info("Setting first event ID") loop.log.Info("Setting first event ID")
event, err := loop.apiClient.GetEvent("") event, err := loop.client().GetEvent("")
if err != nil { if err != nil {
loop.log.WithError(err).Error("Could not get latest event ID") loop.log.WithError(err).Error("Could not get latest event ID")
return return
@ -115,7 +118,7 @@ func (loop *eventLoop) stop() {
} }
} }
func (loop *eventLoop) start() { // nolint[funlen] func (loop *eventLoop) start() {
if loop.isRunning { if loop.isRunning {
return return
} }
@ -134,13 +137,18 @@ func (loop *eventLoop) start() { // nolint[funlen]
loop.log.WithField("lastEventID", loop.currentEventID).Warn("Subscription stopped") loop.log.WithField("lastEventID", loop.currentEventID).Warn("Subscription stopped")
}() }()
t := time.NewTicker(pollInterval - pollIntervalSpread)
defer t.Stop()
loop.hasInternet = true loop.hasInternet = true
go loop.pollNow() 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 { for {
var eventProcessedCh chan struct{} var eventProcessedCh chan struct{}
select { select {
@ -245,10 +253,16 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
loop.pollCounter++ loop.pollCounter++
var event *pmapi.Event 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") 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) l = l.WithField("newEventID", event.EventID)
if !loop.hasInternet { if !loop.hasInternet {
@ -326,7 +340,7 @@ func (loop *eventLoop) processAddresses(log *logrus.Entry, addressEvents []*pmap
log.Debug("Processing address change event") log.Debug("Processing address change event")
// Get old addresses for comparisons before updating user. // Get old addresses for comparisons before updating user.
oldList := loop.apiClient.Addresses() oldList := loop.client().Addresses()
if err = loop.user.UpdateUser(); err != nil { if err = loop.user.UpdateUser(); err != nil {
if logoutErr := loop.user.Logout(); logoutErr != nil { if logoutErr := loop.user.Logout(); logoutErr != nil {
@ -368,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") return errors.Wrap(err, "failed to update address IDs in store")
} }
@ -435,7 +449,7 @@ func (loop *eventLoop) processMessages(eventLog *logrus.Entry, messages []*pmapi
msgLog.WithError(err).Warning("Message was not present in DB. Trying fetch...") msgLog.WithError(err).Warning("Message was not present in DB. Trying fetch...")
if msg, err = loop.apiClient.GetMessage(message.ID); err != nil { if msg, err = loop.client().GetMessage(message.ID); err != nil {
if _, ok := err.(*pmapi.ErrUnprocessableEntity); ok { if _, ok := err.(*pmapi.ErrUnprocessableEntity); ok {
msgLog.WithError(err).Warn("Skipping message update because message exists neither in local DB nor on API") msgLog.WithError(err).Warn("Skipping message update because message exists neither in local DB nor on API")
err = nil err = nil
@ -509,14 +523,7 @@ func updateMessage(msgLog *logrus.Entry, message *pmapi.Message, updates *pmapi.
message.LabelIDs = updates.LabelIDs message.LabelIDs = updates.LabelIDs
} else { } else {
for _, added := range updates.LabelIDsAdded { for _, added := range updates.LabelIDsAdded {
hasLabel := false if !message.HasLabelID(added) {
for _, l := range message.LabelIDs {
if added == l {
hasLabel = true
break
}
}
if !hasLabel {
msgLog.WithField("added", added).Trace("Adding label to message") msgLog.WithField("added", added).Trace("Adding label to message")
message.LabelIDs = append(message.LabelIDs, added) message.LabelIDs = append(message.LabelIDs, added)
} }

View File

@ -39,21 +39,21 @@ func TestEventLoopProcessMoreEvents(t *testing.T) {
// Doesn't matter which IDs are used. // Doesn't matter which IDs are used.
// This test is trying to see whether event loop will immediately process // This test is trying to see whether event loop will immediately process
// next event if there is `More` of them. // 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", EventID: "event50",
More: 1, More: 1,
}, nil), }, nil),
m.api.EXPECT().GetEvent("event50").Return(&pmapi.Event{ m.client.EXPECT().GetEvent("event50").Return(&pmapi.Event{
EventID: "event70", EventID: "event70",
More: 0, More: 0,
}, nil), }, nil),
m.api.EXPECT().GetEvent("event70").Return(&pmapi.Event{ m.client.EXPECT().GetEvent("event70").Return(&pmapi.Event{
EventID: "event71", EventID: "event71",
More: 0, More: 0,
}, nil), }, nil),
) )
m.newStoreNoEvents(true) 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. // Event loop runs in goroutine and will be stopped by deferred mock clearing.
go m.store.eventLoop.start() go m.store.eventLoop.start()
@ -78,12 +78,12 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) {
newSubject := "new subject" newSubject := "new subject"
// First sync will add message with old subject to database. // 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", ID: "msg1",
Subject: subject, Subject: subject,
}, nil) }, nil)
// Event will update the subject. // Event will update the subject.
m.api.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{ m.client.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{
EventID: "event1", EventID: "event1",
Messages: []*pmapi.EventMessage{{ Messages: []*pmapi.EventMessage{{
EventItem: pmapi.EventItem{ EventItem: pmapi.EventItem{

View File

@ -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 // 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. // 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 { if err != nil || total != 0 {
return return
} }
@ -258,8 +258,8 @@ func (storeMailbox *Mailbox) pollNow() {
} }
// api is a proxy for the store's `PMAPIProvider`. // api is a proxy for the store's `PMAPIProvider`.
func (storeMailbox *Mailbox) api() PMAPIProvider { func (storeMailbox *Mailbox) client() pmapi.Client {
return storeMailbox.store.api return storeMailbox.store.client()
} }
// update is a proxy for the store's db's `Update`. // update is a proxy for the store's db's `Update`.

View File

@ -28,15 +28,15 @@ import (
) )
// GetCounts returns numbers of total and unread messages in this mailbox bucket. // 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 { 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 err
}) })
return 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 // 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 // we also need to retrieve the count of unread emails therefore we are
// looping all messages in this mailbox by `bolt.Cursor` // 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++ total++
rawMsg := metaBucket.Get(apiID) rawMsg := metaBucket.Get(apiID)
if rawMsg == nil { if rawMsg == nil {
return 0, 0, ErrNoSuchAPIID return 0, 0, 0, ErrNoSuchAPIID
} }
// Do not unmarshal whole JSON to speed up the looping. // Do not unmarshal whole JSON to speed up the looping.
// Instead, we assume it will contain JSON int field `Unread` // Instead, we assume it will contain JSON int field `Unread`
// where `1` means true (i.e. message is unread) // where `1` means true (i.e. message is unread)
if bytes.Contains(rawMsg, []byte(`"Unread":1`)) { if bytes.Contains(rawMsg, []byte(`"Unread":1`)) {
if unseenSeqNum == 0 {
unseenSeqNum = total
}
unread++ unread++
} }
} }
return total, unread, err return total, unread, unseenSeqNum, err
} }
type mailboxCounts struct { type mailboxCounts struct {
@ -217,7 +220,7 @@ func (store *Store) txGetOnAPICounts(tx *bolt.Tx) ([]*mailboxCounts, error) {
// createOrUpdateOnAPICounts will change only on-API-counts. // createOrUpdateOnAPICounts will change only on-API-counts.
func (store *Store) createOrUpdateOnAPICounts(mailboxCountsOnAPI []*pmapi.MessagesCount) error { 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 { tx := func(tx *bolt.Tx) error {
countsBkt := tx.Bucket(countsBucket) countsBkt := tx.Bucket(countsBucket)

View File

@ -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. // in PM message. Message-Id in normal copy/move will be the PM internal ID.
messageID := header.Get("Message-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 // 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. // Message-ID will be reflected by ExternalID in API message meta-data.
externalID := strings.Trim(messageID, "<> ") // remove '<>' to improve match externalID := strings.Trim(messageID, "<> ") // remove '<>' to improve match

View File

@ -24,6 +24,8 @@ import (
bolt "go.etcd.io/bbolt" 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` // GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage`
// tied to this mailbox. // tied to this mailbox.
func (storeMailbox *Mailbox) GetMessage(apiID string) (*Message, error) { 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 // FetchMessage fetches the message with the given `apiID`, stores it in the database, and returns a new store message
// wrapping it. // wrapping it.
func (storeMailbox *Mailbox) FetchMessage(apiID string) (*Message, error) { func (storeMailbox *Mailbox) FetchMessage(apiID string) (*Message, error) {
msg, err := storeMailbox.api().GetMessage(apiID) msg, err := storeMailbox.client().GetMessage(apiID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -62,7 +64,7 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe
LabelIDs: labelIDs, LabelIDs: labelIDs,
} }
res, err := storeMailbox.api().Import([]*pmapi.ImportMsgReq{importReqs}) res, err := storeMailbox.client().Import([]*pmapi.ImportMsgReq{importReqs})
if err == nil && len(res) > 0 { if err == nil && len(res) > 0 {
msg.ID = res[0].MessageID msg.ID = res[0].MessageID
} }
@ -78,8 +80,16 @@ func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error {
"label": storeMailbox.labelID, "label": storeMailbox.labelID,
"mailbox": storeMailbox.Name, "mailbox": storeMailbox.Name,
}).Trace("Labeling messages") }).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() 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. // UnlabelMessages removes the label by calling an API.
@ -91,8 +101,11 @@ func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error {
"label": storeMailbox.labelID, "label": storeMailbox.labelID,
"mailbox": storeMailbox.Name, "mailbox": storeMailbox.Name,
}).Trace("Unlabeling messages") }).Trace("Unlabeling messages")
if storeMailbox.labelID == pmapi.AllMailLabel {
return ErrAllMailOpNotAllowed
}
defer storeMailbox.pollNow() 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. // MarkMessagesRead marks the message read by calling an API.
@ -116,7 +129,7 @@ func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error {
ids = append(ids, apiID) ids = append(ids, apiID)
} }
} }
return storeMailbox.api().MarkMessagesRead(ids) return storeMailbox.client().MarkMessagesRead(ids)
} }
// MarkMessagesUnread marks the message unread by calling an API. // MarkMessagesUnread marks the message unread by calling an API.
@ -128,7 +141,7 @@ func (storeMailbox *Mailbox) MarkMessagesUnread(apiIDs []string) error {
"mailbox": storeMailbox.Name, "mailbox": storeMailbox.Name,
}).Trace("Marking messages as unread") }).Trace("Marking messages as unread")
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.api().MarkMessagesUnread(apiIDs) return storeMailbox.client().MarkMessagesUnread(apiIDs)
} }
// MarkMessagesStarred adds the Starred label by calling an API. // MarkMessagesStarred adds the Starred label by calling an API.
@ -141,7 +154,7 @@ func (storeMailbox *Mailbox) MarkMessagesStarred(apiIDs []string) error {
"mailbox": storeMailbox.Name, "mailbox": storeMailbox.Name,
}).Trace("Marking messages as starred") }).Trace("Marking messages as starred")
defer storeMailbox.pollNow() 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. // MarkMessagesUnstarred removes the Starred label by calling an API.
@ -154,7 +167,7 @@ func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error {
"mailbox": storeMailbox.Name, "mailbox": storeMailbox.Name,
}).Trace("Marking messages as unstarred") }).Trace("Marking messages as unstarred")
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.api().UnlabelMessages(apiIDs, pmapi.StarredLabel) return storeMailbox.client().UnlabelMessages(apiIDs, pmapi.StarredLabel)
} }
// DeleteMessages deletes messages. // DeleteMessages deletes messages.
@ -197,21 +210,21 @@ func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
} }
} }
if len(messageIDsToUnlabel) > 0 { 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") log.WithError(err).Warning("Cannot unlabel before deleting")
} }
} }
if len(messageIDsToDelete) > 0 { if len(messageIDsToDelete) > 0 {
if err := storeMailbox.api().DeleteMessages(messageIDsToDelete); err != nil { if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
return err return err
} }
} }
case pmapi.DraftLabel: case pmapi.DraftLabel:
if err := storeMailbox.api().DeleteMessages(apiIDs); err != nil { if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
return err return err
} }
default: default:
if err := storeMailbox.api().UnlabelMessages(apiIDs, storeMailbox.labelID); err != nil { if err := storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID); err != nil {
return err return err
} }
} }
@ -290,7 +303,6 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
seqNum, seqNum,
msg, msg,
) )
shouldSendMailboxUpdate = true
} }
continue continue
} }
@ -376,7 +388,7 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
} }
func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error { func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
total, unread, err := storeMailbox.txGetCounts(tx) total, unread, unreadSeqNum, err := storeMailbox.txGetCounts(tx)
if err != nil { if err != nil {
return errors.Wrap(err, "cannot get counts for mailbox status update") return errors.Wrap(err, "cannot get counts for mailbox status update")
} }
@ -385,6 +397,7 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
storeMailbox.labelName, storeMailbox.labelName,
total, total,
unread, unread,
unreadSeqNum,
) )
return nil return nil
} }

View File

@ -28,7 +28,6 @@ import (
// a specific mailbox with helper functions to get IMAP UID, sequence // a specific mailbox with helper functions to get IMAP UID, sequence
// numbers and similar. // numbers and similar.
type Message struct { type Message struct {
api PMAPIProvider
msg *pmapi.Message msg *pmapi.Message
store *Store store *Store
@ -37,7 +36,6 @@ type Message struct {
func newStoreMessage(storeMailbox *Mailbox, msg *pmapi.Message) *Message { func newStoreMessage(storeMailbox *Mailbox, msg *pmapi.Message) *Message {
return &Message{ return &Message{
api: storeMailbox.store.api,
msg: msg, msg: msg,
store: storeMailbox.store, store: storeMailbox.store,
storeMailbox: storeMailbox, storeMailbox: storeMailbox,

View File

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mocks package mocks
@ -7,6 +7,7 @@ package mocks
import ( import (
reflect "reflect" reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )
@ -45,6 +46,43 @@ func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic)) 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 // MockBridgeUser is a mock of BridgeUser interface
type MockBridgeUser struct { type MockBridgeUser struct {
ctrl *gomock.Controller ctrl *gomock.Controller

View File

@ -26,6 +26,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -89,10 +90,10 @@ var (
// Store is local user storage, which handles the synchronization between IMAP and PM API. // Store is local user storage, which handles the synchronization between IMAP and PM API.
type Store struct { type Store struct {
panicHandler PanicHandler panicHandler PanicHandler
eventLoop *eventLoop eventLoop *eventLoop
user BridgeUser user BridgeUser
api PMAPIProvider clientManager ClientManager
log *logrus.Entry log *logrus.Entry
@ -101,7 +102,7 @@ type Store struct {
db *bolt.DB db *bolt.DB
lock *sync.RWMutex lock *sync.RWMutex
addresses map[string]*Address addresses map[string]*Address
imapUpdates chan interface{} imapUpdates chan imapBackend.Update
isSyncRunning bool isSyncRunning bool
syncCooldown cooldown syncCooldown cooldown
@ -112,13 +113,13 @@ type Store struct {
func New( func New(
panicHandler PanicHandler, panicHandler PanicHandler,
user BridgeUser, user BridgeUser,
api PMAPIProvider, clientManager ClientManager,
events listener.Listener, events listener.Listener,
path string, path string,
cache *Cache, cache *Cache,
) (store *Store, err error) { ) (store *Store, err error) {
if user == nil || api == nil || events == nil || cache == nil { if user == nil || clientManager == nil || events == nil || cache == nil {
return nil, fmt.Errorf("missing parameters - user: %v, api: %v, events: %v, cache: %v", user, api, events, cache) return nil, fmt.Errorf("missing parameters - user: %v, api: %v, events: %v, cache: %v", user, clientManager, events, cache)
} }
l := log.WithField("user", user.ID()) l := log.WithField("user", user.ID())
@ -139,14 +140,14 @@ func New(
} }
store = &Store{ store = &Store{
panicHandler: panicHandler, panicHandler: panicHandler,
api: api, clientManager: clientManager,
user: user, user: user,
cache: cache, cache: cache,
filePath: path, filePath: path,
db: bdb, db: bdb,
lock: &sync.RWMutex{}, lock: &sync.RWMutex{},
log: l, log: l,
} }
// Minimal increase is event pollInterval, doubles every failed retry up to 5 minutes. // Minimal increase is event pollInterval, doubles every failed retry up to 5 minutes.
@ -162,7 +163,7 @@ func New(
} }
if user.IsConnected() { if user.IsConnected() {
store.eventLoop = newEventLoop(cache, store, api, user, events) store.eventLoop = newEventLoop(cache, store, user, events)
go func() { go func() {
defer store.panicHandler.HandlePanic() defer store.panicHandler.HandlePanic()
store.eventLoop.start() store.eventLoop.start()
@ -265,10 +266,14 @@ func (store *Store) init(firstInit bool) (err error) {
return err 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 // 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. // the API is unavailable for whatever reason it tries to fetch the labels locally.
func (store *Store) initCounts() (labels []*pmapi.Label, err error) { 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.") store.log.WithError(err).Warn("Could not list API labels. Trying with local labels.")
if labels, err = store.getLabelsFromLocalStorage(); err != nil { if labels, err = store.getLabelsFromLocalStorage(); err != nil {
store.log.WithError(err).Error("Cannot list local labels") store.log.WithError(err).Error("Cannot list local labels")

View File

@ -24,9 +24,9 @@ import (
"sync" "sync"
"testing" "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" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -43,12 +43,13 @@ const (
type mocksForStore struct { type mocksForStore struct {
tb testing.TB tb testing.TB
ctrl *gomock.Controller ctrl *gomock.Controller
events *storeMocks.MockListener events *storemocks.MockListener
api *bridgemocks.MockPMAPIProvider user *storemocks.MockBridgeUser
user *storeMocks.MockBridgeUser client *pmapimocks.MockClient
panicHandler *storeMocks.MockPanicHandler clientManager *storemocks.MockClientManager
store *Store panicHandler *storemocks.MockPanicHandler
store *Store
tmpDir string tmpDir string
cache *Cache cache *Cache
@ -57,12 +58,13 @@ type mocksForStore struct {
func initMocks(tb testing.TB) (*mocksForStore, func()) { func initMocks(tb testing.TB) (*mocksForStore, func()) {
ctrl := gomock.NewController(tb) ctrl := gomock.NewController(tb)
mocks := &mocksForStore{ mocks := &mocksForStore{
tb: tb, tb: tb,
ctrl: ctrl, ctrl: ctrl,
events: storeMocks.NewMockListener(ctrl), events: storemocks.NewMockListener(ctrl),
api: bridgemocks.NewMockPMAPIProvider(ctrl), user: storemocks.NewMockBridgeUser(ctrl),
user: storeMocks.NewMockBridgeUser(ctrl), client: pmapimocks.NewMockClient(ctrl),
panicHandler: storeMocks.NewMockPanicHandler(ctrl), clientManager: storemocks.NewMockClientManager(ctrl),
panicHandler: storemocks.NewMockPanicHandler(ctrl),
} }
// Called during clean-up. // 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().IsConnected().Return(true)
mocks.user.EXPECT().IsCombinedAddressMode().Return(combinedMode) 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: addrID1, Email: addr1, Type: pmapi.OriginalAddress, Receive: pmapi.CanReceive},
{ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: pmapi.CanReceive}, {ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: pmapi.CanReceive},
}) })
mocks.api.EXPECT().ListLabels() mocks.client.EXPECT().ListLabels()
mocks.api.EXPECT().CountMessages("") mocks.client.EXPECT().CountMessages("")
mocks.api.EXPECT().GetEvent(gomock.Any()). mocks.client.EXPECT().GetEvent(gomock.Any()).
Return(&pmapi.Event{ Return(&pmapi.Event{
EventID: "latestEventID", EventID: "latestEventID",
}, nil).AnyTimes() }, nil).AnyTimes()
@ -106,7 +110,7 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unpar
// We want to wait until first sync has finished. // We want to wait until first sync has finished.
firstSyncWaiter := sync.WaitGroup{} firstSyncWaiter := sync.WaitGroup{}
firstSyncWaiter.Add(1) firstSyncWaiter.Add(1)
mocks.api.EXPECT(). mocks.client.EXPECT().
ListMessages(gomock.Any()). ListMessages(gomock.Any()).
DoAndReturn(func(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error) { DoAndReturn(func(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
firstSyncWaiter.Done() firstSyncWaiter.Done()
@ -117,7 +121,7 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unpar
mocks.store, err = New( mocks.store, err = New(
mocks.panicHandler, mocks.panicHandler,
mocks.user, mocks.user,
mocks.api, mocks.clientManager,
mocks.events, mocks.events,
filepath.Join(mocks.tmpDir, "mailbox-test.db"), filepath.Join(mocks.tmpDir, "mailbox-test.db"),
mocks.cache, mocks.cache,

View File

@ -28,6 +28,17 @@ import (
// TestSync triggers a sync of the store. // TestSync triggers a sync of the store.
func (store *Store) TestSync() { 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() store.triggerSync()
} }
@ -46,6 +57,11 @@ func (store *Store) TestGetEventLoop() *eventLoop { //nolint[golint]
return store.eventLoop 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. // TestGetStoreFilePath returns the filepath of the store's database file.
func (store *Store) TestGetStoreFilePath() string { func (store *Store) TestGetStoreFilePath() string {
return store.filePath return store.filePath
@ -53,6 +69,12 @@ func (store *Store) TestGetStoreFilePath() string {
// TestDumpDB will dump store database content. // TestDumpDB will dump store database content.
func (store *Store) TestDumpDB(tb assert.TestingT) { 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 dumpCounts := true
fmt.Printf(">>>>>>>> DUMP %s <<<<<\n\n", store.db.Path()) fmt.Printf(">>>>>>>> DUMP %s <<<<<\n\n", store.db.Path())

View File

@ -42,7 +42,7 @@ type messageLister interface {
ListMessages(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error) 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 labelID := pmapi.AllMailLabel
// When the full sync starts (i.e. is not already in progress), we need to load // 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") 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") return errors.Wrap(err, "failed to load IDs ranges")
} }
syncState.save() syncState.save()
@ -71,7 +71,7 @@ func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api message
defer panicHandler.HandlePanic() defer panicHandler.HandlePanic()
defer wg.Done() defer wg.Done()
err := syncBatch(labelID, store, api, syncState, idRange, &shouldStop) err := syncBatch(labelID, store, api(), syncState, idRange, &shouldStop)
if err != nil { if err != nil {
shouldStop = 1 shouldStop = 1
resultError = errors.Wrap(err, "failed to sync group") resultError = errors.Wrap(err, "failed to sync group")

View File

@ -26,7 +26,7 @@ import (
) )
func TestSyncState_IDRanges(t *testing.T) { func TestSyncState_IDRanges(t *testing.T) {
store := &mockStoreSynchronizer{} store := newSyncer()
syncState := newSyncState(store, 0, []*syncIDRange{}, []string{}) syncState := newSyncState(store, 0, []*syncIDRange{}, []string{})
syncState.initIDRanges() syncState.initIDRanges()
@ -43,7 +43,7 @@ func TestSyncState_IDRanges(t *testing.T) {
} }
func TestSyncState_IDRangesLoaded(t *testing.T) { func TestSyncState_IDRangesLoaded(t *testing.T) {
store := &mockStoreSynchronizer{} store := newSyncer()
syncState := newSyncState(store, 0, []*syncIDRange{ syncState := newSyncState(store, 0, []*syncIDRange{
{StartID: "", StopID: "100"}, {StartID: "", StopID: "100"},
{StartID: "100", StopID: ""}, {StartID: "100", StopID: ""},
@ -57,9 +57,9 @@ func TestSyncState_IDRangesLoaded(t *testing.T) {
} }
func TestSyncState_IDsToBeDeleted(t *testing.T) { func TestSyncState_IDsToBeDeleted(t *testing.T) {
store := &mockStoreSynchronizer{ store := newSyncer()
allMessageIDs: generateIDs(1, 9), store.allMessageIDs = generateIDs(1, 9)
}
syncState := newSyncState(store, 0, []*syncIDRange{}, []string{}) syncState := newSyncState(store, 0, []*syncIDRange{}, []string{})
require.Nil(t, syncState.loadMessageIDsToBeDeleted()) require.Nil(t, syncState.loadMessageIDsToBeDeleted())
@ -74,9 +74,9 @@ func TestSyncState_IDsToBeDeleted(t *testing.T) {
} }
func TestSyncState_IDsToBeDeletedLoaded(t *testing.T) { func TestSyncState_IDsToBeDeletedLoaded(t *testing.T) {
store := &mockStoreSynchronizer{ store := newSyncer()
allMessageIDs: generateIDs(1, 9), store.allMessageIDs = generateIDs(1, 9)
}
syncState := newSyncState(store, 0, []*syncIDRange{}, generateIDs(4, 9)) syncState := newSyncState(store, 0, []*syncIDRange{}, generateIDs(4, 9))
idsToBeDeleted := syncState.getIDsToBeDeleted() idsToBeDeleted := syncState.getIDsToBeDeleted()

View File

@ -20,6 +20,7 @@ package store
import ( import (
"sort" "sort"
"strconv" "strconv"
"sync"
"testing" "testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -79,16 +80,29 @@ func (m *mockLister) ListMessages(filter *pmapi.MessagesFilter) (msgs []*pmapi.M
} }
type mockStoreSynchronizer struct { type mockStoreSynchronizer struct {
locker sync.Locker
allMessageIDs []string allMessageIDs []string
errCreateOrUpdateMessagesEvent error errCreateOrUpdateMessagesEvent error
createdMessageIDsByBatch [][]string createdMessageIDsByBatch [][]string
} }
func newSyncer() *mockStoreSynchronizer {
return &mockStoreSynchronizer{
locker: &sync.Mutex{},
}
}
func (m *mockStoreSynchronizer) getAllMessageIDs() ([]string, error) { func (m *mockStoreSynchronizer) getAllMessageIDs() ([]string, error) {
m.locker.Lock()
defer m.locker.Unlock()
return m.allMessageIDs, nil return m.allMessageIDs, nil
} }
func (m *mockStoreSynchronizer) createOrUpdateMessagesEvent(messages []*pmapi.Message) error { func (m *mockStoreSynchronizer) createOrUpdateMessagesEvent(messages []*pmapi.Message) error {
m.locker.Lock()
defer m.locker.Unlock()
if m.errCreateOrUpdateMessagesEvent != nil { if m.errCreateOrUpdateMessagesEvent != nil {
return m.errCreateOrUpdateMessagesEvent return m.errCreateOrUpdateMessagesEvent
} }
@ -101,10 +115,15 @@ func (m *mockStoreSynchronizer) createOrUpdateMessagesEvent(messages []*pmapi.Me
} }
func (m *mockStoreSynchronizer) deleteMessagesEvent([]string) error { func (m *mockStoreSynchronizer) deleteMessagesEvent([]string) error {
m.locker.Lock()
defer m.locker.Unlock()
return nil return nil
} }
func (m *mockStoreSynchronizer) saveSyncState(finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) { func (m *mockStoreSynchronizer) saveSyncState(finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) {
m.locker.Lock()
defer m.locker.Unlock()
} }
func newTestSyncState(store storeSynchronizer, splitIDs ...string) *syncState { func newTestSyncState(store storeSynchronizer, splitIDs ...string) *syncState {
@ -173,12 +192,12 @@ func TestSyncAllMail(t *testing.T) { //nolint[funlen]
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
store := &mockStoreSynchronizer{ store := newSyncer()
allMessageIDs: generateIDs(1, numberOfMessages+10), store.allMessageIDs = generateIDs(1, numberOfMessages+10)
}
syncState := newSyncState(store, 0, tc.idRanges, tc.idsToBeDeleted) 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) require.Nil(t, err)
// Check all messages were created or updated. // Check all messages were created or updated.
@ -217,16 +236,16 @@ func TestSyncAllMail_FailedListing(t *testing.T) {
numberOfMessages := 10000 numberOfMessages := 10000
store := &mockStoreSynchronizer{ store := newSyncer()
allMessageIDs: generateIDs(1, numberOfMessages+10), store.allMessageIDs = generateIDs(1, numberOfMessages+10)
}
api := &mockLister{ api := &mockLister{
err: errors.New("error"), err: errors.New("error"),
messageIDs: generateIDs(1, numberOfMessages), messageIDs: generateIDs(1, numberOfMessages),
} }
syncState := newTestSyncState(store) 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") 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 numberOfMessages := 10000
store := &mockStoreSynchronizer{ store := newSyncer()
errCreateOrUpdateMessagesEvent: errors.New("error"), store.errCreateOrUpdateMessagesEvent = errors.New("error")
allMessageIDs: generateIDs(1, numberOfMessages+10), store.allMessageIDs = generateIDs(1, numberOfMessages+10)
}
api := &mockLister{ api := &mockLister{
messageIDs: generateIDs(1, numberOfMessages), messageIDs: generateIDs(1, numberOfMessages),
} }
syncState := newTestSyncState(store) 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") require.EqualError(t, err, "failed to sync group: failed to create or update messages: error")
} }
func TestFindIDRanges(t *testing.T) { //nolint[funlen] func TestFindIDRanges(t *testing.T) { //nolint[funlen]
store := &mockStoreSynchronizer{} store := newSyncer()
syncState := newTestSyncState(store) syncState := newTestSyncState(store)
tests := []struct { tests := []struct {
@ -343,7 +362,7 @@ func TestFindIDRanges(t *testing.T) { //nolint[funlen]
} }
func TestFindIDRanges_FailedListing(t *testing.T) { func TestFindIDRanges_FailedListing(t *testing.T) {
store := &mockStoreSynchronizer{} store := newSyncer()
api := &mockLister{ api := &mockLister{
err: errors.New("error"), err: errors.New("error"),
} }
@ -466,7 +485,7 @@ func TestSyncBatch(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
store := &mockStoreSynchronizer{} store := newSyncer()
api := &mockLister{ api := &mockLister{
messageIDs: generateIDs(1, 1000), messageIDs: generateIDs(1, 1000),
} }
@ -479,7 +498,7 @@ func TestSyncBatch(t *testing.T) {
} }
func TestSyncBatch_FailedListing(t *testing.T) { func TestSyncBatch_FailedListing(t *testing.T) {
store := &mockStoreSynchronizer{} store := newSyncer()
api := &mockLister{ api := &mockLister{
err: errors.New("error"), err: errors.New("error"),
messageIDs: generateIDs(1, 1000), messageIDs: generateIDs(1, 1000),
@ -490,9 +509,8 @@ func TestSyncBatch_FailedListing(t *testing.T) {
} }
func TestSyncBatch_FailedCreateOrUpdateMessage(t *testing.T) { func TestSyncBatch_FailedCreateOrUpdateMessage(t *testing.T) {
store := &mockStoreSynchronizer{ store := newSyncer()
errCreateOrUpdateMessagesEvent: errors.New("error"), store.errCreateOrUpdateMessagesEvent = errors.New("error")
}
api := &mockLister{ api := &mockLister{
messageIDs: generateIDs(1, 1000), messageIDs: generateIDs(1, 1000),
} }

View File

@ -17,42 +17,14 @@
package store package store
import ( import "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"io"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type PanicHandler interface { type PanicHandler interface {
HandlePanic() HandlePanic()
} }
// PMAPIProvider is subset of pmapi.Client for use by the Store. type ClientManager interface {
type PMAPIProvider interface { GetClient(userID string) pmapi.Client
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
} }
// BridgeUser is subset of bridge.User for use by the Store. // BridgeUser is subset of bridge.User for use by the Store.

View File

@ -24,7 +24,7 @@ func (store *Store) UserID() string {
// GetSpace returns used and total space in bytes. // GetSpace returns used and total space in bytes.
func (store *Store) GetSpace() (usedSpace, maxSpace uint, err error) { func (store *Store) GetSpace() (usedSpace, maxSpace uint, err error) {
apiUser, err := store.api.CurrentUser() apiUser, err := store.client().CurrentUser()
if err != nil { if err != nil {
return 0, 0, err 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. // GetMaxUpload returns max size of attachment in bytes.
func (store *Store) GetMaxUpload() (uint, error) { func (store *Store) GetMaxUpload() (uint, error) {
apiUser, err := store.api.CurrentUser() apiUser, err := store.client().CurrentUser()
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -46,6 +46,8 @@ func (store *Store) RebuildMailboxes() (err error) {
log.WithField("user", store.UserID()).Trace("Truncating mailboxes") log.WithField("user", store.UserID()).Trace("Truncating mailboxes")
store.addresses = nil
if err = store.truncateMailboxesBucket(); err != nil { if err = store.truncateMailboxesBucket(); err != nil {
log.WithError(err).Error("Could not truncate mailboxes bucket") log.WithError(err).Error("Could not truncate mailboxes bucket")
return return
@ -127,7 +129,12 @@ func (store *Store) createOrDeleteAddressesEvent() (err error) {
delete(store.addresses, addr.addressID) 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. // truncateAddressInfoBucket removes the address info bucket.
@ -153,8 +160,6 @@ func (store *Store) truncateAddressInfoBucket() (err error) {
func (store *Store) truncateMailboxesBucket() (err error) { func (store *Store) truncateMailboxesBucket() (err error) {
log.Trace("Truncating mailboxes bucket") log.Trace("Truncating mailboxes bucket")
store.addresses = nil
tx := func(tx *bolt.Tx) (err error) { tx := func(tx *bolt.Tx) (err error) {
mbs := tx.Bucket(mailboxesBucket) mbs := tx.Bucket(mailboxesBucket)

View File

@ -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. // 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 { if addressList == nil {
err = errors.New("addresses unavailable") err = errors.New("addresses unavailable")
store.log.WithError(err).Error("Could not get user addresses from API") store.log.WithError(err).Error("Could not get user addresses from API")

View File

@ -55,7 +55,7 @@ func (store *Store) createMailbox(name string) error {
return nil return nil
} }
_, err := store.api.CreateLabel(&pmapi.Label{ _, err := store.client().CreateLabel(&pmapi.Label{
Name: name, Name: name,
Color: color, Color: color,
Exclusive: exclusive, Exclusive: exclusive,
@ -133,7 +133,7 @@ func (store *Store) leastUsedColor() string {
func (store *Store) updateMailbox(labelID, newName, color string) error { func (store *Store) updateMailbox(labelID, newName, color string) error {
defer store.eventLoop.pollNow() defer store.eventLoop.pollNow()
_, err := store.api.UpdateLabel(&pmapi.Label{ _, err := store.client().UpdateLabel(&pmapi.Label{
ID: labelID, ID: labelID,
Name: newName, Name: newName,
Color: color, Color: color,
@ -150,15 +150,15 @@ func (store *Store) deleteMailbox(labelID, addressID string) error {
var err error var err error
switch labelID { switch labelID {
case pmapi.SpamLabel: case pmapi.SpamLabel:
err = store.api.EmptyFolder(pmapi.SpamLabel, addressID) err = store.client().EmptyFolder(pmapi.SpamLabel, addressID)
case pmapi.TrashLabel: case pmapi.TrashLabel:
err = store.api.EmptyFolder(pmapi.TrashLabel, addressID) err = store.client().EmptyFolder(pmapi.TrashLabel, addressID)
default: default:
err = fmt.Errorf("cannot empty mailbox %v", labelID) err = fmt.Errorf("cannot empty mailbox %v", labelID)
} }
return err return err
} }
return store.api.DeleteLabel(labelID) return store.client().DeleteLabel(labelID)
} }
func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error { func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error {
@ -173,7 +173,7 @@ func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) erro
return nil return nil
} }
labels, err := store.api.ListLabels() labels, err := store.client().ListLabels()
if err != nil { if err != nil {
return err return err
} }

View File

@ -26,7 +26,7 @@ import (
"net/textproto" "net/textproto"
"strings" "strings"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -37,7 +37,7 @@ import (
// If `attachedPublicKey` is passed, it's added to attachments. // If `attachedPublicKey` is passed, it's added to attachments.
// Both draft and attachments are encrypted with passed `kr` key. // Both draft and attachments are encrypted with passed `kr` key.
func (store *Store) CreateDraft( func (store *Store) CreateDraft(
kr *pmcrypto.KeyRing, kr *crypto.KeyRing,
message *pmapi.Message, message *pmapi.Message,
attachmentReaders []io.Reader, attachmentReaders []io.Reader,
attachedPublicKey, attachedPublicKey,
@ -54,7 +54,7 @@ func (store *Store) CreateDraft(
message.Attachments = nil message.Attachments = nil
draftAction := store.getDraftAction(message) draftAction := store.getDraftAction(message)
draft, err := store.api.CreateDraft(message, parentID, draftAction) draft, err := store.client().CreateDraft(message, parentID, draftAction)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "failed to create draft") 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 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) r := bytes.NewReader(attachmentBody)
sigReader, err := attachment.DetachedSign(kr, r) sigReader, err := attachment.DetachedSign(kr, r)
if err != nil { 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") 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 { if err != nil {
return nil, errors.Wrap(err, "failed to create attachment") 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. // SendMessage sends the message.
func (store *Store) SendMessage(messageID string, req *pmapi.SendMessageReq) error { func (store *Store) SendMessage(messageID string, req *pmapi.SendMessageReq) error {
defer store.eventLoop.pollNow() defer store.eventLoop.pollNow()
_, _, err := store.api.SendMessage(messageID, req) _, _, err := store.client().SendMessage(messageID, req)
return err return err
} }

View File

@ -80,7 +80,7 @@ func TestCreateOrUpdateMessageMetadata(t *testing.T) {
a.Equal(t, []*pmapi.Attachment(nil), msg.Attachments) a.Equal(t, []*pmapi.Attachment(nil), msg.Attachments)
a.Equal(t, int64(-1), msg.Size) a.Equal(t, int64(-1), msg.Size)
a.Equal(t, "", msg.MIMEType) 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. // Change the calculated data.
wantSize := int64(42) wantSize := int64(42)

View File

@ -34,7 +34,7 @@ const syncIDsToBeDeletedKey = "ids_to_be_deleted"
// updateCountsFromServer will download and set the counts. // updateCountsFromServer will download and set the counts.
func (store *Store) updateCountsFromServer() error { func (store *Store) updateCountsFromServer() error {
counts, err := store.api.CountMessages("") counts, err := store.client().CountMessages("")
if err != nil { if err != nil {
return errors.Wrap(err, "cannot update counts from server") 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 { if err != nil {
errW := errors.Wrap(err, "cannot count messages") errW := errors.Wrap(err, "cannot count messages")
store.log. store.log.
@ -152,7 +152,7 @@ func (store *Store) triggerSync() {
store.log.WithField("isIncomplete", syncState.isIncomplete()).Info("Store sync started") 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 { if err != nil {
log.WithError(err).Error("Store sync failed") log.WithError(err).Error("Store sync failed")
store.syncCooldown.increaseWaitTime() store.syncCooldown.increaseWaitTime()

View File

@ -27,16 +27,20 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const sep = "\x00" const (
sep = "\x00"
itemLengthBridge = 9
itemLengthImportExport = 6 // Old format for Import/Export.
)
var ( 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 { type Credentials struct {
@ -86,7 +90,7 @@ func (s *Credentials) Unmarshal(secret string) error {
} }
items := strings.Split(string(b), sep) items := strings.Split(string(b), sep)
if len(items) != 9 { if len(items) != itemLengthBridge && len(items) != itemLengthImportExport {
return ErrWrongFormat return ErrWrongFormat
} }
@ -94,16 +98,26 @@ func (s *Credentials) Unmarshal(secret string) error {
s.Emails = items[1] s.Emails = items[1]
s.APIToken = items[2] s.APIToken = items[2]
s.MailboxPassword = items[3] s.MailboxPassword = items[3]
s.BridgePassword = items[4]
s.Version = items[5] switch len(items) {
if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil { case itemLengthBridge:
s.Timestamp = 0 s.BridgePassword = items[4]
} s.Version = items[5]
if s.IsHidden = false; items[7] == "1" { if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil {
s.IsHidden = true s.Timestamp = 0
} }
if s.IsCombinedAddressMode = false; items[8] == "1" { if s.IsHidden = false; items[7] == "1" {
s.IsCombinedAddressMode = true s.IsHidden = true
}
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 return nil
} }

View 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)
}

View File

@ -18,7 +18,6 @@
package credentials package credentials
import ( import (
"errors"
"fmt" "fmt"
"sort" "sort"
"sync" "sync"
@ -36,8 +35,8 @@ type Store struct {
} }
// NewStore creates a new encrypted credentials store. // NewStore creates a new encrypted credentials store.
func NewStore() (*Store, error) { func NewStore(appName string) (*Store, error) {
secrets, err := keychain.NewAccess("bridge") secrets, err := keychain.NewAccess(appName)
return &Store{ return &Store{
secrets: secrets, secrets: secrets,
}, err }, err
@ -67,18 +66,9 @@ func (s *Store) Add(userID, userName, apiToken, mailboxPassword string, emails [
creds.SetEmailList(emails) creds.SetEmailList(emails)
var has bool currentCredentials, err := s.get(userID)
if has, err = s.has(userID); err != nil { if err == nil {
log.WithField("userID", userID).WithError(err).Error("Could not check if user credentials already exist")
return
}
if has {
log.Info("Updating credentials of existing user") log.Info("Updating credentials of existing user")
currentCredentials, err := s.get(userID)
if err != nil {
return nil, err
}
creds.BridgePassword = currentCredentials.BridgePassword creds.BridgePassword = currentCredentials.BridgePassword
creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode creds.IsCombinedAddressMode = currentCredentials.IsCombinedAddressMode
creds.Timestamp = currentCredentials.Timestamp creds.Timestamp = currentCredentials.Timestamp
@ -125,6 +115,20 @@ func (s *Store) UpdateEmails(userID string, emails []string) error {
return s.saveCredentials(credentials) return s.saveCredentials(credentials)
} }
func (s *Store) UpdatePassword(userID, password string) error {
storeLocker.Lock()
defer storeLocker.Unlock()
credentials, err := s.get(userID)
if err != nil {
return err
}
credentials.MailboxPassword = password
return s.saveCredentials(credentials)
}
func (s *Store) UpdateToken(userID, apiToken string) error { func (s *Store) UpdateToken(userID, apiToken string) error {
storeLocker.Lock() storeLocker.Lock()
defer storeLocker.Unlock() defer storeLocker.Unlock()
@ -225,40 +229,9 @@ func (s *Store) Get(userID string) (creds *Credentials, err error) {
storeLocker.RLock() storeLocker.RLock()
defer storeLocker.RUnlock() defer storeLocker.RUnlock()
var has bool
if has, err = s.has(userID); err != nil {
log.WithError(err).Error("Could not check for credentials")
return
}
if !has {
err = errors.New("no credentials found for given userID")
return
}
return s.get(userID) return s.get(userID)
} }
func (s *Store) has(userID string) (has bool, err error) {
if err = s.checkKeychain(); err != nil {
return
}
var ids []string
if ids, err = s.secrets.List(); err != nil {
log.WithError(err).Error("Could not list credentials")
return
}
for _, id := range ids {
if id == userID {
has = true
}
}
return
}
func (s *Store) get(userID string) (creds *Credentials, err error) { func (s *Store) get(userID string) (creds *Credentials, err error) {
log := log.WithField("user", userID) log := log.WithField("user", userID)

View File

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

View 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
View 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
}

View File

@ -15,61 +15,54 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"fmt"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend"
"github.com/pkg/errors" "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. // 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("bridge account is logged out, use bridge to login again") 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. // User is a struct on top of API client and credentials store.
type User struct { type User struct {
log *logrus.Entry log *logrus.Entry
panicHandler PanicHandler panicHandler PanicHandler
listener listener.Listener listener listener.Listener
apiClient PMAPIProvider clientManager ClientManager
credStorer CredentialsStorer credStorer CredentialsStorer
imapUpdatesChannel chan interface{} imapUpdatesChannel chan imapBackend.Update
store *store.Store storeFactory StoreMaker
storeCache *store.Cache store *store.Store
storePath string
userID string userID string
creds *credentials.Credentials creds *credentials.Credentials
lock sync.RWMutex lock sync.RWMutex
authChannel chan *pmapi.Auth isAuthorized bool
hasAPIAuth bool
unlockingKeyringLock sync.Mutex
wasKeyringUnlocked bool
} }
// newUser creates a new bridge user. // newUser creates a new user.
func newUser( func newUser(
panicHandler PanicHandler, panicHandler PanicHandler,
userID string, userID string,
eventListener listener.Listener, eventListener listener.Listener,
credStorer CredentialsStorer, credStorer CredentialsStorer,
apiClient PMAPIProvider, clientManager ClientManager,
storeCache *store.Cache, storeFactory StoreMaker,
storeDir string,
) (u *User, err error) { ) (u *User, err error) {
log := log.WithField("user", userID) log := log.WithField("user", userID)
log.Debug("Creating or loading user") log.Debug("Creating or loading user")
@ -80,33 +73,30 @@ func newUser(
} }
u = &User{ u = &User{
log: log, log: log,
panicHandler: panicHandler, panicHandler: panicHandler,
listener: eventListener, listener: eventListener,
credStorer: credStorer, credStorer: credStorer,
apiClient: apiClient, clientManager: clientManager,
storeCache: storeCache, storeFactory: storeFactory,
storePath: getUserStorePath(storeDir, userID), userID: userID,
userID: userID, creds: creds,
creds: creds,
} }
return 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 // (such as when logging out and back in, you need to reload the credentials because the new credentials will
// have the apitoken and password), authorising the user against the api, loading the user store (creating a new one // have the apitoken and password), authorising the user against the api, loading the user store (creating a new one
// if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if // if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if
// something in the store changed). // something in the store changed).
func (u *User) init(idleUpdates chan interface{}, apiClient PMAPIProvider) (err error) { func (u *User) init(idleUpdates chan imapBackend.Update) (err error) {
// If this is an existing user, we still need a new api client to get a new refresh token. u.log.Info("Initialising user")
// If it's a new user, doesn't matter really; this is basically a noop in this case.
u.apiClient = apiClient
u.unlockingKeyringLock.Lock()
u.wasKeyringUnlocked = false
u.unlockingKeyringLock.Unlock()
// Reload the user's credentials (if they log out and back in we need the new // Reload the user's credentials (if they log out and back in we need the new
// version with the apitoken and mailbox password). // version with the apitoken and mailbox password).
@ -116,17 +106,8 @@ func (u *User) init(idleUpdates chan interface{}, apiClient PMAPIProvider) (err
} }
u.creds = creds u.creds = creds
// Set up the auth channel on which auths from the api client are sent.
u.authChannel = make(chan *pmapi.Auth)
u.apiClient.SetAuths(u.authChannel)
u.hasAPIAuth = false
go func() {
defer u.panicHandler.HandlePanic()
u.watchAPIClientAuths()
}()
// Try to authorise the user if they aren't already authorised. // Try to authorise the user if they aren't already authorised.
// Note: we still allow users to set up 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 { if authErr := u.authorizeIfNecessary(false); authErr != nil {
switch errors.Cause(authErr) { switch errors.Cause(authErr) {
case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser: case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser:
@ -147,7 +128,7 @@ func (u *User) init(idleUpdates chan interface{}, apiClient PMAPIProvider) (err
} }
u.store = nil 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 { if err != nil {
return errors.Wrap(err, "failed to create store") 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. // authorizeIfNecessary checks whether user is logged in and is connected to api auth channel.
// If user is not already connected to the api auth channel (for example there was no internet during start), // If user is not already connected to the api auth channel (for example there was no internet during start),
// it tries to connect it. See `connectToAuthChannel` for more info. // it tries to connect it.
func (u *User) authorizeIfNecessary(emitEvent bool) (err error) { func (u *User) authorizeIfNecessary(emitEvent bool) (err error) {
// If user is connected and has an auth channel, then perfect, nothing to do here. // If user is connected and has an auth channel, then perfect, nothing to do here.
if u.creds.IsConnected() && u.HasAPIAuth() { if u.creds.IsConnected() && u.isAuthorized {
// The keyring unlock is triggered here to resolve state where apiClient // The keyring unlock is triggered here to resolve state where apiClient
// is authenticated (we have auth token) but it was not possible to download // is authenticated (we have auth token) but it was not possible to download
// and unlock the keys (internet not reachable). // and unlock the keys (internet not reachable).
@ -209,22 +190,14 @@ func (u *User) authorizeIfNecessary(emitEvent bool) (err error) {
// unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked. // unlockIfNecessary will not trigger keyring unlocking if it was already successfully unlocked.
func (u *User) unlockIfNecessary() error { func (u *User) unlockIfNecessary() error {
u.unlockingKeyringLock.Lock() if u.client().IsUnlocked() {
defer u.unlockingKeyringLock.Unlock()
if u.wasKeyringUnlocked {
return nil 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") return errors.Wrap(err, "failed to unlock user")
} }
if err := u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return errors.Wrap(err, "failed to unlock user addresses")
}
u.wasKeyringUnlocked = true
return nil return nil
} }
@ -236,49 +209,28 @@ func (u *User) authorizeAndUnlock() (err error) {
return nil return nil
} }
auth, err := u.apiClient.AuthRefresh(u.creds.APIToken) if _, err := u.client().AuthRefresh(u.creds.APIToken); err != nil {
if err != nil {
return errors.Wrap(err, "failed to refresh API auth") return errors.Wrap(err, "failed to refresh API auth")
} }
u.authChannel <- auth
if _, err = u.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") return errors.Wrap(err, "failed to unlock user")
} }
if err = u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil {
return errors.Wrap(err, "failed to unlock user addresses")
}
return nil return nil
} }
// See `connectToAPIClientAuthChannel` for more info. func (u *User) updateAuthToken(auth *pmapi.Auth) {
func (u *User) watchAPIClientAuths() { u.log.Debug("User received auth")
for auth := range u.authChannel {
if auth != nil { if err := u.credStorer.UpdateToken(u.userID, auth.GenToken()); err != nil {
newRefreshToken := auth.UID() + ":" + auth.RefreshToken u.log.WithError(err).Error("Failed to update refresh token in credentials store")
u.updateAPIToken(newRefreshToken) return
u.hasAPIAuth = true
} else if err := u.logout(); err != nil {
u.log.WithError(err).Error("Cannot logout user after receiving empty auth from API")
}
} }
}
// updateAPIToken is helper for updating the token in keychain. It's not supposed to be u.refreshFromCredentials()
// 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") u.isAuthorized = true
if err := u.credStorer.UpdateToken(u.userID, newRefreshToken); err != nil {
u.log.WithError(err).Error("Cannot update refresh token in credentials store")
} else {
u.refreshFromCredentials()
}
} }
// clearStore removes the database. // clearStore removes the database.
@ -291,7 +243,7 @@ func (u *User) clearStore() error {
} }
} else { } else {
u.log.Warn("Store is not initialized: cleaning up store files manually") u.log.Warn("Store is not initialized: cleaning up store files manually")
if err := 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") return errors.Wrap(err, "failed to remove store manually")
} }
} }
@ -311,17 +263,11 @@ func (u *User) closeStore() error {
return nil return nil
} }
// getUserStorePath returns the file path of the store database for the given userID.
func getUserStorePath(storeDir string, userID string) (path string) {
fileName := fmt.Sprintf("mailbox-%v.db", userID)
return filepath.Join(storeDir, fileName)
}
// GetTemporaryPMAPIClient returns an authorised PMAPI client. // GetTemporaryPMAPIClient returns an authorised PMAPI client.
// Do not use! It's only for backward compatibility of old SMTP and IMAP implementations. // Do not use! It's only for backward compatibility of old SMTP and IMAP implementations.
// After proper refactor of SMTP and IMAP remove this method. // After proper refactor of SMTP and IMAP remove this method.
func (u *User) GetTemporaryPMAPIClient() PMAPIProvider { func (u *User) GetTemporaryPMAPIClient() pmapi.Client {
return u.apiClient return u.client()
} }
// ID returns the user's userID. // 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 // 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 { func (u *User) CheckBridgeLogin(password string) error {
if isApplicationOutdated { if isApplicationOutdated {
u.listener.Emit(events.UpgradeApplicationEvent, "") u.listener.Emit(events.UpgradeApplicationEvent, "")
@ -462,20 +408,16 @@ func (u *User) UpdateUser() error {
return errors.Wrap(err, "cannot update user") return errors.Wrap(err, "cannot update user")
} }
_, err := u.apiClient.UpdateUser() _, err := u.client().UpdateUser()
if err != nil { if err != nil {
return err return err
} }
if _, err = u.apiClient.Unlock(u.creds.MailboxPassword); err != nil { if err = u.client().ReloadKeys([]byte(u.creds.MailboxPassword)); err != nil {
return err return errors.Wrap(err, "failed to reload keys")
} }
if err := u.apiClient.UnlockAddresses([]byte(u.creds.MailboxPassword)); err != nil { emails := u.client().Addresses().ActiveEmails()
return err
}
emails := u.apiClient.Addresses().ActiveEmails()
if err := u.credStorer.UpdateEmails(u.userID, emails); err != nil { if err := u.credStorer.UpdateEmails(u.userID, emails); err != nil {
return err return err
} }
@ -519,16 +461,21 @@ func (u *User) SwitchAddressMode() (err error) {
} }
// logout is the same as Logout, but for internal purposes (logged out from // logout is the same as Logout, but for internal purposes (logged out from
// the server) which emits LogoutEvent to notify other parts of the Bridge. // the server) which emits LogoutEvent to notify other parts of the app.
func (u *User) logout() error { func (u *User) logout() error {
u.lock.Lock() u.lock.Lock()
wasConnected := u.creds.IsConnected() wasConnected := u.creds.IsConnected()
u.lock.Unlock() u.lock.Unlock()
err := u.Logout() err := u.Logout()
if wasConnected { if wasConnected {
u.listener.Emit(events.LogoutEvent, u.userID) u.listener.Emit(events.LogoutEvent, u.userID)
u.listener.Emit(events.UserRefreshEvent, u.userID) u.listener.Emit(events.UserRefreshEvent, u.userID)
} }
u.isAuthorized = false
return err return err
} }
@ -544,22 +491,7 @@ func (u *User) Logout() (err error) {
return return
} }
u.unlockingKeyringLock.Lock() u.client().Logout()
u.wasKeyringUnlocked = false
u.unlockingKeyringLock.Unlock()
if err = u.apiClient.Logout(); err != nil {
u.log.WithError(err).Warn("Could not log user out from API client")
}
u.apiClient.SetAuths(nil)
// Logout needs to stop auth channel so when user logs back in
// it can register again with new client.
// Note: be careful to not close channel twice.
if u.authChannel != nil {
close(u.authChannel)
u.authChannel = nil
}
if err = u.credStorer.Logout(u.userID); err != nil { if err = u.credStorer.Logout(u.userID); err != nil {
u.log.WithError(err).Warn("Could not log user out from credentials store") u.log.WithError(err).Warn("Could not log user out from credentials store")
@ -575,6 +507,7 @@ func (u *User) Logout() (err error) {
u.closeEventLoop() u.closeEventLoop()
u.closeAllConnections() u.closeAllConnections()
runtime.GC() runtime.GC()
return err return err
@ -582,7 +515,7 @@ func (u *User) Logout() (err error) {
func (u *User) refreshFromCredentials() { func (u *User) refreshFromCredentials() {
if credentials, err := u.credStorer.Get(u.userID); err != nil { if credentials, err := u.credStorer.Get(u.userID); err != nil {
log.Error("Cannot update credentials: ", err) log.WithError(err).Error("Cannot refresh user credentials")
} else { } else {
u.creds = credentials u.creds = credentials
} }
@ -615,7 +548,3 @@ func (u *User) CloseConnection(address string) {
func (u *User) GetStore() *store.Store { func (u *User) GetStore() *store.Store {
return u.store return u.store
} }
func (u *User) HasAPIAuth() bool {
return u.hasAPIAuth
}

View File

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

View 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
View File

@ -0,0 +1,101 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
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
View 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!")
}
}

View 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)
}

View 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
}

View 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)
}

View File

@ -15,27 +15,31 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"runtime/debug"
"testing" "testing"
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
bridgemocks "github.com/ProtonMail/proton-bridge/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
if os.Getenv("VERBOSITY") == "fatal" {
logrus.SetLevel(logrus.FatalLevel)
}
if os.Getenv("VERBOSITY") == "trace" { if os.Getenv("VERBOSITY") == "trace" {
logrus.SetLevel(logrus.TraceLevel) logrus.SetLevel(logrus.TraceLevel)
} }
@ -45,11 +49,9 @@ func TestMain(m *testing.M) {
var ( var (
testAuth = &pmapi.Auth{ //nolint[gochecknoglobals] testAuth = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "tok", RefreshToken: "tok",
KeySalt: "", // No salting in tests.
} }
testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals] testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals]
RefreshToken: "reftok", RefreshToken: "reftok",
KeySalt: "", // No salting in tests.
} }
testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals] testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals]
@ -125,18 +127,39 @@ type mocks struct {
t *testing.T t *testing.T
ctrl *gomock.Controller ctrl *gomock.Controller
config *bridgemocks.MockConfiger config *usersmocks.MockConfiger
PanicHandler *bridgemocks.MockPanicHandler PanicHandler *usersmocks.MockPanicHandler
prefProvider *bridgemocks.MockPreferenceProvider clientManager *usersmocks.MockClientManager
pmapiClient *bridgemocks.MockPMAPIProvider credentialsStore *usersmocks.MockCredentialsStorer
credentialsStore *bridgemocks.MockCredentialsStorer storeMaker *usersmocks.MockStoreMaker
eventListener *MockListener eventListener *MockListener
pmapiClient *pmapimocks.MockClient
storeCache *store.Cache storeCache *store.Cache
} }
type fullStackReporter struct {
T testing.TB
}
func (fr *fullStackReporter) Errorf(format string, args ...interface{}) {
fmt.Printf("err: "+format+"\n", args...)
fr.T.Fail()
}
func (fr *fullStackReporter) Fatalf(format string, args ...interface{}) {
debug.PrintStack()
fmt.Printf("fail: "+format+"\n", args...)
fr.T.FailNow()
}
func initMocks(t *testing.T) mocks { func initMocks(t *testing.T) mocks {
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") cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache") require.NoError(t, err, "could not get temporary file for store cache")
@ -145,84 +168,84 @@ func initMocks(t *testing.T) mocks {
t: t, t: t,
ctrl: mockCtrl, ctrl: mockCtrl,
config: bridgemocks.NewMockConfiger(mockCtrl), config: usersmocks.NewMockConfiger(mockCtrl),
PanicHandler: bridgemocks.NewMockPanicHandler(mockCtrl), PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl),
pmapiClient: bridgemocks.NewMockPMAPIProvider(mockCtrl), clientManager: usersmocks.NewMockClientManager(mockCtrl),
prefProvider: bridgemocks.NewMockPreferenceProvider(mockCtrl), credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl),
credentialsStore: bridgemocks.NewMockCredentialsStorer(mockCtrl), storeMaker: usersmocks.NewMockStoreMaker(mockCtrl),
eventListener: NewMockListener(mockCtrl), eventListener: NewMockListener(mockCtrl),
pmapiClient: pmapimocks.NewMockClient(mockCtrl),
storeCache: store.NewCache(cacheFile.Name()), storeCache: store.NewCache(cacheFile.Name()),
} }
// Ignore heartbeat calls because they always happen.
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Heartbeat), gomock.Any(), gomock.Any()).AnyTimes()
m.prefProvider.EXPECT().Get(preferences.NextHeartbeatKey).AnyTimes()
m.prefProvider.EXPECT().Set(preferences.NextHeartbeatKey, gomock.Any()).AnyTimes()
// Called during clean-up. // Called during clean-up.
m.PanicHandler.EXPECT().HandlePanic().AnyTimes() m.PanicHandler.EXPECT().HandlePanic().AnyTimes()
// Set up store factory.
m.storeMaker.EXPECT().New(gomock.Any()).DoAndReturn(func(user store.BridgeUser) (*store.Store, error) {
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 return m
} }
func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge { func testNewUsersWithUsers(t *testing.T, m mocks) *Users {
// Init for user. // Events are asynchronous
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil) m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).Times(2)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil) m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).Times(2)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil) m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).Times(2)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Init for users. gomock.InOrder(
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil) m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil),
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("users", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil) // Init for user.
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}),
return testNewBridge(t, m) // Init for users.
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),
)
users := testNewUsers(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 { func testNewUsers(t *testing.T, m mocks) *Users { //nolint[unparam]
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db") m.config.EXPECT().GetVersion().Return("ver").AnyTimes()
require.NoError(t, err, "could not get temporary file for store cache")
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(false).AnyTimes()
m.prefProvider.EXPECT().GetBool(preferences.AllowProxyKey).Return(false).AnyTimes()
m.config.EXPECT().GetDBDir().Return("/tmp").AnyTimes()
m.config.EXPECT().GetIMAPCachePath().Return(cacheFile.Name()).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any()).AnyTimes()
m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any()) m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any())
pmapiClientFactory := func(userID string) PMAPIProvider { m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth))
log.WithField("userID", userID).Info("Creating new pmclient")
return m.pmapiClient
}
bridge := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, "ver", pmapiClientFactory, m.credentialsStore) users := New(m.config, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore, m.storeMaker)
waitForEvents() waitForEvents()
return bridge return users
} }
func cleanUpBridgeUserData(b *Bridge) { func cleanUpUsersData(b *Users) {
for _, user := range b.users { for _, user := range b.users {
_ = user.clearStore() _ = user.clearStore()
} }
@ -232,8 +255,11 @@ func TestClearData(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
defer cleanUpBridgeUserData(bridge) 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, "user@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@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) m.config.EXPECT().ClearData().Return(nil)
require.NoError(t, bridge.ClearData()) require.NoError(t, users.ClearData())
waitForEvents() 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),
)
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package users
// IsAuthorized returns whether the user has received an Auth from the API yet.
func (u *User) IsAuthorized() bool {
return u.isAuthorized
}

View File

@ -19,20 +19,16 @@ package config
import ( import (
"io/ioutil" "io/ioutil"
"net"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time"
"github.com/ProtonMail/go-appdir" "github.com/ProtonMail/go-appdir"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/sirupsen/logrus"
) )
var ( var (
log = GetLogEntry("config") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "config") //nolint[gochecknoglobals]
) )
type appDirProvider interface { type appDirProvider interface {
@ -48,7 +44,6 @@ type Config struct {
cacheVersion string cacheVersion string
appDirs appDirProvider appDirs appDirProvider
appDirsVersion appDirProvider appDirsVersion appDirProvider
apiConfig *pmapi.ClientConfig
} }
// New returns fully initialized config struct. // New returns fully initialized config struct.
@ -70,17 +65,6 @@ func newConfig(appName, version, revision, cacheVersion string, appDirs, appDirs
cacheVersion: cacheVersion, cacheVersion: cacheVersion,
appDirs: appDirs, appDirs: appDirs,
appDirsVersion: appDirsVersion, 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) fileName := filepath.Base(filePath)
return (fileName != c.cacheVersion && return (fileName != c.cacheVersion &&
!logFileRgx.MatchString(fileName) && !logFileRgx.MatchString(fileName) &&
filePath != c.GetLogDir() &&
filePath != c.GetTLSCertPath() && filePath != c.GetTLSCertPath() &&
filePath != c.GetTLSKeyPath() && filePath != c.GetTLSKeyPath() &&
filePath != c.GetEventsPath() && filePath != c.GetEventsPath() &&
@ -188,6 +173,11 @@ func (c *Config) IsDevMode() bool {
return os.Getenv("PROTONMAIL_ENV") == "dev" return os.Getenv("PROTONMAIL_ENV") == "dev"
} }
// GetVersion returns the version.
func (c *Config) GetVersion() string {
return c.version
}
// GetLogDir returns folder for log files. // GetLogDir returns folder for log files.
func (c *Config) GetLogDir() string { func (c *Config) GetLogDir() string {
return c.appDirs.UserLogs() return c.appDirs.UserLogs()
@ -238,11 +228,6 @@ func (c *Config) GetPreferencesPath() string {
return filepath.Join(c.appDirsVersion.UserCache(), "prefs.json") 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. // GetDefaultAPIPort returns default Bridge local API port.
func (c *Config) GetDefaultAPIPort() int { func (c *Config) GetDefaultAPIPort() int {
return 1042 return 1042

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