Compare commits

...

117 Commits

Author SHA1 Message Date
1d9855a190 Other: Bridge James 1.8.11 2021-11-12 14:06:04 +01:00
cea33bebe2 GODT-1415: Only messages which are in Spam should be moved to INBOX once they are marked as not-a-spam.
This is regression of GODT-963
2021-11-09 10:32:58 +00:00
9d405a1549 GODT-1397: Update bbolt to v1.3.6 2021-11-09 10:01:08 +00:00
47f468e4b7 GODT-1410: Remove event ID from sentry report description 2021-11-08 16:05:34 +00:00
b9c6c00709 GODT-1395: CI should fail on go.sum changed. 2021-11-05 06:12:38 +01:00
5ce9cb8eec GODT-1405: Integration test fix: Prevent unilateral update in FETCH when copying message by append. 2021-11-01 16:32:28 +01:00
bc7133e401 Merge branch 'master' into devel 2021-10-21 13:26:29 +02:00
a219ecf3cb Other: fix go.sum 2021-10-21 13:25:44 +02:00
8061b1e6fa GODT-1392: added unit test for get message 2021-10-15 14:01:04 +02:00
6509df523f GODT-1360: added extra check.
This should help diagnostic test failure next time it happens.
2021-10-08 09:01:02 +00:00
0d1abaec0d Other: fixed new copy test feature. 2021-10-07 11:31:56 +00:00
4d1ace5de7 Other: Copy from All Mail allowed again.
Creates duplicate.
Added test scenario.
2021-10-07 11:31:56 +00:00
1250621a4d GODT-963 STORE removing junk or adding nojunk should move message to inbox 2021-10-07 11:31:56 +00:00
8d6e55ba54 GODT-965 MOVE command should end with error for All Mail 2021-10-07 11:31:56 +00:00
a4a29cbf82 Other Improved tests in move_without_support 2021-10-07 11:31:56 +00:00
39bccc2747 GODT-968 Messages in All Mail should not be able to mark as deleted
Feature already implemented.
A test already existed, but lacked the final expunge check.
2021-10-07 11:31:56 +00:00
0cf1b38c2b GODT-967 Append external message to All Mail should be APPEND to Archive instead 2021-10-07 11:31:56 +00:00
6b7e706100 GODT-966 Do nothing if message APPEND to All Mail is already in Archive. 2021-10-07 11:31:56 +00:00
bb90ed5446 GODT-966: Fixed comment. 2021-10-07 11:31:56 +00:00
107843d58f GODT-966: return correct UID in response to APPEND to All Mail. 2021-10-07 11:31:56 +00:00
63f089540e GODT-966 Append internal message to AllMail should be no action
GODT-966 Fixed CI issues (WIP)

WIP GODT-966 modified behavior of APPEND.

WIP GODT-966 implemented test for possible order of operations.

WIP GODT-966: code cleanup and refactoring.

WIP GODT-966 fix for linter.

Other: Minor refactoring.
2021-10-07 11:31:56 +00:00
c35ff4f913 Other: James v1.8.10 2021-09-30 22:19:14 +02:00
cd6b5cdcc3 GODT-1348: max 100 conn per host 2021-09-30 21:19:11 +02:00
8f1ca00c9d GODT-1318 go-srp version identifier in go.sum underlying commit is identical 2021-09-30 20:19:07 +02:00
f21f583d05 GODT-1202: Do not update package if it's version older than launcher 2021-09-23 16:24:11 +00:00
e940d9f6fe GODT-1204: Handle importing too big messages 2021-09-23 11:09:56 +00:00
1ed8e939b8 Other: typo 2021-09-20 14:28:57 +02:00
b5d63783f7 GODT-1318: Bump gopenpgp to 2.2.2, go-srp to a843a0b9, go-crypto to 52430bf6 2021-09-17 11:54:55 +02:00
e0113ec267 GODT-219: Update to godog v0.12.1 2021-09-10 13:38:05 +02:00
22d2bcc21d GODT-1205: "RCPT TO" does not contain all addressed from "CC" 2021-09-06 21:13:37 +00:00
91dcb2f773 Other: Bridge James 1.8.9 2021-09-01 12:20:59 +02:00
c676c732ab Merge branch 'release-notes' into devel 2021-09-01 12:13:20 +02:00
444f2d8a12 Other 2021-09-01 07:45:48 +00:00
f10da3c7f0 GODT-1263: Fix crash on invalid or empty header 2021-08-27 13:39:34 +00:00
b8dd9f82bd GODT-1235: Fix 401 response error handling 2021-08-27 12:29:42 +00:00
1157e60972 GODT-1261: Fix building messages with long key 2021-08-16 15:47:14 +02:00
e9e4d8c725 Other: use windows-compatible filename when dumping message in QA builds 2021-08-04 13:05:57 +02:00
186fa24106 Other: Bridge James v 1.8.8 2021-07-21 06:45:47 +02:00
63780b7b8d GODT-1234 Set attachment name 'message.eml' for message/rfc822 attachments. 2021-07-19 14:40:55 +02:00
e3e4769d78 Other: remove dead code 2021-07-19 07:45:56 +02:00
b2e9c4e4e9 Other: Revert "GODT-1224: don't strip trailing newlines from message bodies"
This reverts commit 54161e263fa2f95795fc8623e9dfd2134afb0ae5.
2021-07-19 07:45:29 +02:00
db4cb36538 GODT-1224: don't strip trailing newlines from message bodies 2021-07-19 07:45:12 +02:00
984864553e Other: better keychain logging 2021-07-19 07:44:58 +02:00
2707a5627c Other: prefer empty string check vs nil check 2021-06-25 15:34:48 +02:00
8e0d6d41a6 Other 2021-06-23 08:40:15 +00:00
2b76a45e17 Other 2021-06-21 20:59:37 +00:00
7ab54da8c4 Other: Bridge James 1.8.7 2021-06-17 13:54:43 +02:00
2cce29e858 GODT-1201: bump gopenpgp to 2.1.10 and update crypto time 2021-06-17 05:32:17 +00:00
ef1223391b GODT-1193: Don't doubly encode parts 2021-06-17 07:04:43 +02:00
fb98a797ba Merge branch 'release/james' into devel 2021-06-15 15:56:31 +02:00
6be31bdeb2 Other go.mod 2021-06-15 15:55:02 +02:00
fce5990d19 Other: release notes 1.8.5 2021-06-15 08:57:59 +02:00
1d4ee0c33e Other: Bridge 1.8.6 2021-06-14 09:07:50 +02:00
a3e102e456 GODT-1166: Do not expose current auth token in interface 2021-06-11 17:13:26 +02:00
21dcac9fac GODT-1187: Remove IMAP/SMTP blocking when no internet. 2021-06-11 09:16:47 +00:00
f0ee82fdd2 GODT-1166: Preliminary disable IMAP/SMTP blocking feature. 2021-06-11 09:16:47 +00:00
0c6a098af9 GODT-1166: Reduce the number of auth for live test
- Changed: Do not reauth controller clients.
- Changed: Verbosisty is set only once before run
- Changed: AddUser takes TestAccount as argument
- Added: Setup/clean up before/after test run
- Added: Access to the current refresh token from pmapi.Client interface.
- Added: Context function to add test a user to bridge without login, just call users.FinishLogin.
- Added: PMAPIController.GetAuthClient returns authenticated client for username.
- Added: Persistent clients does not loggout after every scenario.
- Changed: Disabled no-internet tests.
2021-06-11 09:16:47 +00:00
5bf359d34f GODT-1193: don't use message.Read; permit non-UTF-8 charsets 2021-06-11 06:43:21 +00:00
6d784f2444 Other: release notes 1.8.5 2021-06-11 08:33:54 +02:00
835bf1e77f Other: make changelog linter happy 2021-06-09 09:51:31 +02:00
df5fbda72f Other: Bridge James 1.8.5 2021-06-08 23:54:31 +02:00
c482f768d9 GODT-1189 GODT-1190 GODT-1191 Fix missing sender while creating draft. 2021-06-08 09:09:32 +02:00
21cf7459c9 Other: Bridge 1.8.4 Changelog 2021-06-02 11:14:19 +02:00
cf1ba6588a GODT-949: Fix section parsing issue 2021-06-02 05:56:17 +02:00
858f2c7f29 Other: add (failing) bodystructure test 2021-06-01 11:01:14 +00:00
f63238faed Other: stuff mostly passes but bodystructure parse is broken? 2021-06-01 11:01:14 +00:00
f6ff85f69d GODT-1184: Preserve signatures in externally signed messages 2021-06-01 11:01:14 +00:00
ec5b5939b9 GODT-949: Add comment about ignoring InvalidMediaParameter 2021-06-01 09:04:05 +00:00
dec00ff9cc GODT-949: Ignore some InvalidMediaParameter errors in lite parser 2021-06-01 10:54:01 +02:00
9fddd77f0d GODT-1183: Add test for getting contact emails by email 2021-05-31 12:16:26 +00:00
aae65c9d38 Other: fix license year in QML 2021-05-31 13:48:01 +02:00
f3b197fa56 Merge branch 'release/james' into devel 2021-05-28 10:18:46 +02:00
0a9ce5f526 GODT-1155: zero out mailbox password during logout 2021-05-27 16:48:45 +02:00
a2029002c4 GODT-1155 Update gopenpgp and use go-srp 2021-05-27 16:43:44 +02:00
7c41c8e23a Other: Bridge James 1.8.3 2021-05-27 15:13:04 +02:00
36fdb88d96 GODT-1182: use correct contacts route 2021-05-27 14:15:00 +02:00
c69239ca16 Other: bump go-rfc5322 dependency to v0.8.0 2021-05-27 11:03:49 +02:00
e10aa89313 Other: Bridge James 1.8.3 2021-05-26 17:21:33 +02:00
d0a97a3f4a GODT-1044: fix header lines parsing 2021-05-26 14:48:46 +00:00
e01dc77a61 GODT-1044: lite parser 2021-05-26 14:48:46 +00:00
509ba52ba2 GODT-1162: Fix wrong section 1 error when email has no MIME parts 2021-05-26 13:10:05 +00:00
c37a0338c5 Other: Release notes 1.8.2 2021-05-26 13:33:10 +02:00
9f23d5a6f4 Merge branch 'release/james' into devel 2021-05-26 09:42:43 +02:00
885fb95454 Other: Bridge James v1.8.2 2021-05-21 07:16:17 +02:00
629d6c5e4d GODT-1175: report bug 2021-05-20 15:24:43 +02:00
3f50bf66f4 Merge branch 'release/james' into devel 2021-05-20 08:45:23 +02:00
4072205709 Other: version bump and changelog Bridge James 1.8.2 2021-05-20 08:38:33 +02:00
233c55ab19 Other: release notes Bridge James v1.8.1 2021-05-20 08:21:56 +02:00
5d82c218ca GODT-1165: Handle UID FETCH with sequence range of empty mailbox 2021-05-19 16:17:19 +02:00
cb30dd91e3 Other: fix no internet integration test 2021-05-19 08:45:59 +00:00
41d82e10f9 Other: Bridge James v1.8.0 release notes 2021-05-19 10:00:18 +02:00
8496c9e181 Other: bump SMTP test timeout time from 1 to 2 seconds, fingers crossed 2021-05-18 16:55:03 +02:00
3dadad5131 GODT-1161: Guarantee order of responses when creating new message 2021-05-17 17:54:28 +02:00
00146e7474 Other: Bridge James 1.8.0 release notes 2021-05-12 09:04:37 +02:00
12ac47e949 Other: fix typos regarding listener 2021-05-10 15:51:47 +02:00
6ff4c8a738 Other: Bridge James 1.8.0 2021-05-07 15:56:11 +02:00
dd66b7f8d0 GODT-1159 SMTP server not restarting after restored internet
- [x] write tests to check that IMAP and SMTP servers are closed when there
  is no internet
- [x] always create new go-smtp instance during listenAndServe(int)
2021-05-07 10:34:14 +00:00
0b95ed4dea GODT-1146: Refactor header filtering 2021-05-03 15:53:46 +02:00
ce64aeb05f Other: avoid API jail 2021-05-03 07:05:15 +02:00
27cfda680d GODT-1152: Correctly resolve wildcard sequence/UID set 2021-04-30 10:35:34 +00:00
323303a98b GODT-1089 Explicitly open system preferences window on BigSur. 2021-04-30 09:10:14 +00:00
8109831c07 GODT-35: Finish all details and make tests pass 2021-04-30 05:41:39 +02:00
2284e9ede1 GODT-35: New pmapi client and manager using resty 2021-04-30 05:34:36 +02:00
1d538e8540 GODT-876 Set default from if empty for importing draft 2021-04-29 14:07:20 +00:00
8ccaac8090 GODT-1056 Check encrypted size of the message before upload 2021-04-29 12:40:29 +00:00
22bf8f62ce GODT-1143 Turn off SMTP server while no connection 2021-04-28 11:23:41 +02:00
fed031ebaa Other: early release notes 1.7.1 2021-04-28 10:43:42 +02:00
7a15ebbd54 Other: update go.mod with qt docs 2021-04-27 17:11:23 +02:00
94b5799ba7 Other refactor: clean old builder 2021-04-27 08:12:50 +00:00
286f51a4e7 Other: Bridge Iron 1.7.1 2021-04-23 11:29:03 +02:00
ee961ae4a8 GODT-1141 Use attachment name from content type if not specified in content disposition 2021-04-23 07:18:41 +00:00
4038752a9a Other: preserve message header in PGP/MIME passthrough message 2021-04-22 16:30:29 +02:00
ebf724412b Other: fix custom message on decryption error for externally encrypted message 2021-04-22 12:28:54 +00:00
14d42b5e76 GODT-1081 Return newline after headers with every fetch 2021-04-21 13:02:23 +00:00
2b8d92e82d Other: fix release notes 2021-04-21 12:11:52 +02:00
11b1e3acf5 Other: Release notes Iron early 1.7.0 2021-04-21 10:47:46 +02:00
c5eb660315 Other: fix live test: API sanitize timestamp 2021-04-16 08:32:51 +02:00
321 changed files with 12059 additions and 11393 deletions

View File

@ -85,11 +85,17 @@ dependency-updates:
stage: build stage: build
only: only:
- branches - branches
before_script:
- mkdir -p .cache/bin
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
script: script:
- make build - make build
- git diff && git diff-index --quiet HEAD
artifacts: artifacts:
# Note: The latest artifacts for refs are locked against deletion, and kept regardless of the expiry time. # Note: The latest artifacts for refs are locked against deletion, and kept
# Introduced in GitLab 13.0 behind a disabled feature flag, and made the default behavior in GitLab 13.4. # regardless of the expiry time. Introduced in GitLab 13.0 behind a
# disabled feature flag, and made the default behavior in GitLab 13.4.
expire_in: 1 day expire_in: 1 day
tags: tags:
- large - large
@ -187,77 +193,6 @@ build-ie-darwin-qa:
paths: paths:
- ie_*.tgz - ie_*.tgz
.build-windows-base:
extends: .build-base
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
build-windows:
extends: .build-windows-base
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows make build
artifacts:
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-windows-qa:
extends: .build-windows-base
only:
- web
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows BUILD_TAGS="build_qa" make build
artifacts:
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-ie-windows:
extends: .build-windows-base
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows make build-ie
artifacts:
name: "ie-windows-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
build-ie-windows-qa:
extends: .build-windows-base
only:
- web
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows BUILD_TAGS="build_qa" make build-ie
artifacts:
name: "ie-windows-qa-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
# Stage: MIRROR # Stage: MIRROR
mirror-repo: mirror-repo:

View File

@ -2,17 +2,148 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.8.11] James
### Fixed
* GODT-1415: Only messages which are in Spam should be moved to INBOX once they are marked as not-a-spam.
* GODT-1405: Integration test fix: Prevent unilateral update in FETCH when copying message by append.
* GODT-1392: Fix broken header fields for attachments.
* GODT-1360: Fix live integration test.
* GODT-968: Messages in All Mail should not be able to mark as deleted.
* GODT-967: Append external message to All Mail should be APPEND to Archive instead.
* GODT-966: Append internal message to AllMail should be no action.
* GODT-965: MOVE command should end with error for All Mail.
* GODT-963: STORE removing junk or adding nojunk should move message to inbox.
### Changed
* GODT-1397: Update bbolt to v1.3.6.
* GODT-1410: Remove event ID from sentry report description.
* GODT-1395: CI should fail on go.sum changed.
## [Bridge 1.8.10] James
### Fixed
* GODT-1348: Max 100 conn per host.
* GODT-1204: Handle importing too big messages.
* GODT-1202: Do not update package if it's version older than launcher.
* GODT-1318: Bump gopenpgp to v2.2.2, go-srp to v0.0.1, go-crypto to 52430bf6.
* GODT-219: Update to godog v0.12.1.
* GODT-1205: "RCPT TO" does not contain all addressed from "CC".
* GODT-1103: Cleanup on windows when uninstalling Bridge.
## [Bridge 1.8.9] James
### Fixed
* GODT-1263: Fix crash on invalid or empty header.
* GODT-1235: Fix 401 response error handling.
* GODT-1261: Fix building messages with long key.
* Other: use windows-compatible filename when dumping message in QA builds.
## [Bridge 1.8.8] James
### Changed
* GODT-1234 Set attachment name 'message.eml' for `message/rfc822` attachments.
## [Bridge 1.8.7] James
### Changed
* GODT-1201: Update gopenpgp to 2.1.10.
### Fixed
* GODT-1193: Do not doubly encode parts.
## [Bridge 1.8.6] James
### Removed
* GODT-1187: Remove IMAP/SMTP blocking when no internet.
### Changed
* GODT-1166: Reduce the number of auth for live test.
### Fixed
* GODT-1193: Do not use message.Read permit non-UTF-8 charsets.
## [Bridge 1.8.5] James
### Fixed
* GODT-1189: Draft created on Outlook is synced on web.
* GODT-1190: Fix some random crashes of Bridge on Windows.
* GODT-1191: Fix data loss of some drafts messages when restarting outlook on Windows.
## [Bridge 1.8.4] James
### Added
* GODT-1155: Update gopenpgp v2.1.9 and use go-srp.
* GODT-1044: Lite parser for appended messages.
* GODT-1183: Add test for getting contact emails by email.
* GODT-1184: Preserve signatures in externally signed messages.
### Changed
* GODT-949: Ignore some InvalidMediaParameter errors in lite parser.
### Fixed
* GODT-1161: Guarantee order of responses when creating new message.
* GODT-1162: Fix wrong section 1 error when email has no MIME parts.
## [Bridge 1.8.3] James
### Fixed
* GODT-1182: Use correct contact route.
## [Bridge 1.8.2] James
### Fixed
* GODT-1175: Bug reporting.
## [Bridge 1.8.1] James
### Fixed
* GODT-1165: Handle UID FETCH with sequence range of empty mailbox.
## [Bridge 1.8.0] James
### Added
* GODT-1056 Check encrypted size of the message before upload.
* GODT-1143 Turn off SMTP server while no connection.
* GODT-1089 Explicitly open system preferences window on BigSur.
* GODT-35: Connection manager with resty.
### Fixed
* GODT-1159 SMTP server not restarting after restored internet.
* GODT-1146 Refactor handling of fetching BODY[HEADER] (and similar) regarding trailing newline.
* GODT-1152 Correctly resolve wildcard sequence/UID set.
* GODT-876 Set default from if empty for importing draft.
* Other: Avoid API jail.
## [Bridge 1.7.1] Iron
### Fixed
* GODT-1081 Properly return newlines when returning headers.
* GODT-1150 Externally encrypted messages with missing private key would not be built with custom message.
* GODT-1141 Attachment is named as attachment.bin in some cases.
## [Bridge 1.7.0] Iron ## [Bridge 1.7.0] Iron
### Added ### Added
* GODT-213 New message builder: * GODT-213 New message builder:
* Preserve Content-Type for undecryptable message body. * Preserve Content-Type for undecryptable message body.
* Use application/octet-stream for encrypted parts. * Use application/octet-stream for encrypted parts.
* Force no transfer encoding for embedded message/rfc822 parts. * Force no transfer encoding for embedded message/rfc822 parts.
* Remove dead code GetRelatedHeader/GetRelatedBoundary. * Remove dead code GetRelatedHeader/GetRelatedBoundary.
* Correctly expect text/plain in custom message text parts. * Correctly expect text/plain in custom message text parts.
* Force text/plain for custom message text part. * Force text/plain for custom message text part.
* Complex external encrypted tests (multipart/alternative, message/rfc822 attachment). * Complex external encrypted tests (multipart/alternative, message/rfc822 attachment).
### Fixed ### Fixed
* GODT-1136 DB Cache header from builder and test. * GODT-1136 DB Cache header from builder and test.

View File

@ -10,7 +10,7 @@ TARGET_OS?=${GOOS}
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher .PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
# Keep version hardcoded so app build works also without Git repository. # Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.7.0+git BRIDGE_APP_VERSION?=1.8.11+git
IE_APP_VERSION?=1.3.3+git IE_APP_VERSION?=1.3.3+git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico SRC_ICO:=logo.ico
@ -253,13 +253,17 @@ bench:
coverage: test coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html go tool cover -html=/tmp/coverage.out -o=coverage.html
integration-test-bridge:
${MAKE} -C test test-bridge
mocks: mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/users/mocks/listener_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,IMAPClientProvider > internal/transfer/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client,Manager > pkg/pmapi/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/message Fetcher > pkg/message/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/message Fetcher > pkg/message/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-changelog lint: gofiles lint-golang lint-license lint-changelog

View File

@ -24,6 +24,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
@ -86,7 +87,7 @@ func main() { // nolint[funlen]
versioner := versioner.New(updatesPath) versioner := versioner.New(updatesPath)
exe, err := getPathToExecutable(ExeName, versioner, kr, reporter) exe, err := getPathToUpdatedExecutable(ExeName, versioner, kr, reporter)
if err != nil { if err != nil {
if exe, err = getFallbackExecutable(ExeName, versioner); err != nil { if exe, err = getFallbackExecutable(ExeName, versioner); err != nil {
logrus.WithError(err).Fatal("Failed to find any launchable executable") logrus.WithError(err).Fatal("Failed to find any launchable executable")
@ -142,7 +143,7 @@ func appendLauncherPath(path string, args []string) []string {
return res return res
} }
func getPathToExecutable( func getPathToUpdatedExecutable(
name string, name string,
versioner *versioner.Versioner, versioner *versioner.Versioner,
kr *crypto.KeyRing, kr *crypto.KeyRing,
@ -153,6 +154,11 @@ func getPathToExecutable(
return "", errors.Wrap(err, "failed to list available versions") return "", errors.Wrap(err, "failed to list available versions")
} }
currentVersion, err := semver.StrictNewVersion(constants.Version)
if err != nil {
logrus.WithField("version", constants.Version).WithError(err).Error("Failed to parse current version")
}
for _, version := range versions { for _, version := range versions {
vlog := logrus.WithField("version", version) vlog := logrus.WithField("version", version)
@ -170,6 +176,11 @@ func getPathToExecutable(
continue continue
} }
// Skip versions that are less or equal to launcher version.
if currentVersion != nil && !version.SemVer().GreaterThan(currentVersion) {
continue
}
exe, err := version.GetExecutable(name) exe, err := version.GetExecutable(name)
if err != nil { if err != nil {
vlog.WithError(err).Error("Failed to get executable") vlog.WithError(err).Error("Failed to get executable")
@ -179,7 +190,7 @@ func getPathToExecutable(
return exe, nil return exe, nil
} }
return "", errors.New("no available versions") return "", errors.New("no available newer versions")
} }
func getFallbackExecutable(name string, versioner *versioner.Versioner) (string, error) { func getFallbackExecutable(name string, versioner *versioner.Versioner) (string, error) {

View File

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

32
go.mod
View File

@ -1,33 +1,34 @@
module github.com/ProtonMail/proton-bridge module github.com/ProtonMail/proton-bridge
go 1.13 go 1.15
// These dependencies are `replace`d below, so the version numbers should be ignored. // These dependencies are `replace`d below, so the version numbers should be ignored.
// They are in a separate require block to highlight this. // They are in a separate require block to highlight this.
require ( require (
github.com/docker/docker-credential-helpers v0.6.3 github.com/docker/docker-credential-helpers v0.6.3
github.com/emersion/go-imap v1.0.6 github.com/emersion/go-imap v1.0.6
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
) )
require ( require (
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
github.com/Masterminds/semver/v3 v3.1.0 github.com/Masterminds/semver/v3 v3.1.0
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-rfc5322 v0.5.0 github.com/ProtonMail/go-rfc5322 v0.8.0
github.com/ProtonMail/go-srp v0.0.1
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp/v2 v2.1.3 github.com/ProtonMail/gopenpgp/v2 v2.2.2
github.com/PuerkitoBio/goquery v1.5.1 github.com/PuerkitoBio/goquery v1.5.1
github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
github.com/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.12.1
github.com/cucumber/messages-go/v16 v16.0.1
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
@ -40,7 +41,7 @@ require (
github.com/fatih/color v1.9.0 github.com/fatih/color v1.9.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/getsentry/sentry-go v0.8.0 github.com/getsentry/sentry-go v0.8.0
github.com/go-resty/resty/v2 v2.3.0 github.com/go-resty/resty/v2 v2.6.0
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1 github.com/google/go-cmp v0.5.1
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
@ -50,25 +51,28 @@ require (
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.30 github.com/miekg/dns v1.1.41
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.7.0
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/urfave/cli/v2 v2.2.0 github.com/urfave/cli/v2 v2.2.0
github.com/vmihailenco/msgpack/v5 v5.1.3 github.com/vmihailenco/msgpack/v5 v5.1.3
go.etcd.io/bbolt v1.3.5 go.etcd.io/bbolt v1.3.6
golang.org/x/net v0.0.0-20200707034311-ab3426394381 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec
) )
replace ( replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57
) )

343
go.sum
View File

@ -1,3 +1,16 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
@ -8,28 +21,35 @@ github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMd
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57 h1:pHA4K54ifoogVLunGGHi3xyF5Nz4x+Uh3dJuy3NwGQQ=
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c h1:iaVbEOnskSGgcH7XQWHG6VPirHDRoYe+Idd0/dl4m8A= github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2 h1:pQkjJELHayW59jp7r4G5Dlmnicr5McejDfwsjcwI1SU= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2/go.mod h1:HTM9X7e9oLwn7RiqLG0UVwVRJenLs3wN+tQ0NPAfwMQ= github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c h1:FP7mMdsXy0ybzar1sJeIcZtaJka0U/ZmLTW4wRpolYk=
github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs= github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs=
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-rfc5322 v0.5.0 h1:LbKWjgfvumYZCr8BgGyTUk3ETGkFLAjQdkuSUpZ5CcE= github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU=
github.com/ProtonMail/go-rfc5322 v0.5.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU= github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us=
github.com/ProtonMail/go-srp v0.0.0-20210910093455-a843a0b9adff h1:eiue56XAPSkOpsy5Fwnyz4+Vd7i2cN5D4orc++Irt1g=
github.com/ProtonMail/go-srp v0.0.0-20210910093455-a843a0b9adff/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA=
github.com/ProtonMail/go-srp v0.0.1 h1:J0O9Zb5XTC6iDrB7feH41cu+TUEB+l7uHctXIK6oS2o=
github.com/ProtonMail/go-srp v0.0.1/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp/v2 v2.1.3 h1:4+nFDJ9WtcUQTip/je2Ll3P21XhAUl4asWsafLrw97c= github.com/ProtonMail/gopenpgp/v2 v2.2.2 h1:u2m7xt+CZWj88qK1UUNBoXeJCFJwJCZ/Ff4ymGoxEXs=
github.com/ProtonMail/gopenpgp/v2 v2.1.3/go.mod h1:WeYndoqEcRR4/QbgRL24z6OwYX5T1RWerRk8NfZ6rJM= github.com/ProtonMail/gopenpgp/v2 v2.2.2/go.mod h1:ajUlBGvxMH1UBZnaYO3d1FSVzjiC6kK9XlZYGiDCvpM=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
@ -38,29 +58,53 @@ github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzg
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA= github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U= github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY= github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y= github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/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 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8= github.com/cronokirby/saferith v0.31.0 h1:TIlhldetKLeGAb19bZvWiuwQEzfzwSPthDEyJ9Ah8xs=
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/cronokirby/saferith v0.31.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw=
github.com/cucumber/godog v0.12.1 h1:IhWVYFKDReM5WsuA9AuRLRPWOyvFNO9UBUKrNfLPais=
github.com/cucumber/godog v0.12.1/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc=
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g= github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -69,12 +113,11 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc= github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/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-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 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-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc= github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
@ -83,21 +126,18 @@ github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I= github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI= github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b h1:xYuhW6egTaCP+zjbUcfoy/Dr3ASdVPR9W7fmkHvZHPE=
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.14.0 h1:RYW203p+EcPjL8Z/ZpT9lZ6iOc8MG1MQzEx1UKEkXlA= github.com/emersion/go-smtp v0.14.0 h1:RYW203p+EcPjL8Z/ZpT9lZ6iOc8MG1MQzEx1UKEkXlA=
github.com/emersion/go-smtp v0.14.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-smtp v0.14.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro= github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI= github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
@ -107,40 +147,96 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/getsentry/sentry-go v0.8.0 h1:F52cjBVLuiTfdW6p4JFuxlt3pOjKfWYT/aka7cdJ7v0= github.com/getsentry/sentry-go v0.8.0 h1:F52cjBVLuiTfdW6p4JFuxlt3pOjKfWYT/aka7cdJ7v0=
github.com/getsentry/sentry-go v0.8.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= github.com/getsentry/sentry-go v0.8.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.3.0 h1:xdXq34gBOMEloa9rlGStLxmfX/dyIK8htOv36dQUwHU=
github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@ -151,9 +247,13 @@ github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0Gqw
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
@ -163,12 +263,17 @@ github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubc
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg= github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY= github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE= github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/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/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -178,12 +283,12 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/martinlindhe/base36 v1.1.0 h1:cIwvvwYse/0+1CkUPYH5ZvVIYG3JrILmQEIbLuar02Y= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
@ -193,61 +298,95 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -258,10 +397,17 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@ -280,88 +426,201 @@ github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec h1:A1qYjneJuzBZZ2gIB8rd6zrfq6l7SoEMJ8EsSilNK/U= golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec h1:A1qYjneJuzBZZ2gIB8rd6zrfq6l7SoEMJ8EsSilNK/U=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@ -23,7 +23,7 @@
// - persistent settings // - persistent settings
// - event listener // - event listener
// - credentials store // - credentials store
// - pmapi ClientManager // - pmapi Manager
// In addition, the base initialises logging and reacts to command line arguments // In addition, the base initialises logging and reacts to command line arguments
// which control the log verbosity and enable cpu/memory profiling. // which control the log verbosity and enable cpu/memory profiling.
package base package base
@ -85,7 +85,7 @@ type Base struct {
Cache *cache.Cache Cache *cache.Cache
Listener listener.Listener Listener listener.Listener
Creds *credentials.Store Creds *credentials.Store
CM *pmapi.ClientManager CM pmapi.Manager
CookieJar *cookies.Jar CookieJar *cookies.Jar
UserAgent *useragent.UserAgent UserAgent *useragent.UserAgent
Updater *updater.Updater Updater *updater.Updater
@ -181,13 +181,23 @@ func New( // nolint[funlen]
kc = keychain.NewMissingKeychain() kc = keychain.NewMissingKeychain()
} }
cfg := pmapi.NewConfig(configName, constants.Version)
cfg.GetUserAgent = userAgent.String
cfg.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
cfg.TLSIssueHandler = func() { listener.Emit(events.TLSCertIssue, "") }
cm := pmapi.New(cfg)
cm.AddConnectionObserver(pmapi.NewConnectionObserver(
func() { listener.Emit(events.InternetOffEvent, "") },
func() { listener.Emit(events.InternetOnEvent, "") },
))
jar, err := cookies.NewCookieJar(settingsObj) jar, err := cookies.NewCookieJar(settingsObj)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cm := pmapi.NewClientManager(getAPIConfig(configName, listener), userAgent)
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
cm.SetCookieJar(jar) cm.SetCookieJar(jar)
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey) key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
@ -328,6 +338,7 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
} }
logging.SetLevel(c.String(flagLogLevel)) logging.SetLevel(c.String(flagLogLevel))
b.CM.SetLogging(logrus.WithField("pkg", "pmapi"), logrus.GetLevel() == logrus.TraceLevel)
logrus. logrus.
WithField("appName", b.Name). WithField("appName", b.Name).
@ -375,13 +386,3 @@ func (b *Base) doTeardown() error {
return nil return nil
} }
func getAPIConfig(configName string, listener listener.Listener) *pmapi.ClientConfig {
apiConfig := pmapi.GetAPIConfig(configName, constants.Version)
apiConfig.ConnectionOffHandler = func() { listener.Emit(events.InternetOffEvent, "") }
apiConfig.ConnectionOnHandler = func() { listener.Emit(events.InternetOnEvent, "") }
apiConfig.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
return apiConfig
}

View File

@ -95,6 +95,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
smtpPort := b.Settings.GetInt(settings.SMTPPortKey) smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
useSSL := b.Settings.GetBool(settings.SMTPSSLKey) useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
smtp.NewSMTPServer( smtp.NewSMTPServer(
b.CrashHandler,
c.Bool(flagLogSMTP), c.Bool(flagLogSMTP),
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe() smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
}() }()

View File

@ -19,6 +19,7 @@
package bridge package bridge
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"time" "time"
@ -44,7 +45,7 @@ type Bridge struct {
locations Locator locations Locator
settings SettingsProvider settings SettingsProvider
clientManager users.ClientManager clientManager pmapi.Manager
updater Updater updater Updater
versioner Versioner versioner Versioner
} }
@ -56,7 +57,7 @@ func New(
sentryReporter *sentry.Reporter, sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, clientManager pmapi.Manager,
credStorer users.CredentialsStorer, credStorer users.CredentialsStorer,
updater Updater, updater Updater,
versioner Versioner, versioner Versioner,
@ -67,7 +68,7 @@ func New(
clientManager.AllowProxy() clientManager.AllowProxy()
} }
storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, clientManager, eventListener) storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, eventListener)
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true) u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
b := &Bridge{ b := &Bridge{
Users: u, Users: u,
@ -118,28 +119,15 @@ func (b *Bridge) heartbeat() {
// ReportBug reports a new bug from the user. // ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error { func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
c := b.clientManager.GetAnonymousClient() return b.clientManager.ReportBug(context.Background(), pmapi.ReportBugReq{
defer c.Logout()
title := "[Bridge] Bug"
report := pmapi.ReportReq{
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Browser: emailClient, Browser: emailClient,
Title: title, Title: "[Bridge] Bug",
Description: description, Description: description,
Username: accountName, Username: accountName,
Email: address, Email: address,
} })
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
} }
// GetUpdateChannel returns currently set update channel. // GetUpdateChannel returns currently set update channel.

View File

@ -31,7 +31,6 @@ type storeFactory struct {
cache Cacher cache Cacher
sentryReporter *sentry.Reporter sentryReporter *sentry.Reporter
panicHandler users.PanicHandler panicHandler users.PanicHandler
clientManager users.ClientManager
eventListener listener.Listener eventListener listener.Listener
storeCache *store.Cache storeCache *store.Cache
} }
@ -40,14 +39,12 @@ func newStoreFactory(
cache Cacher, cache Cacher,
sentryReporter *sentry.Reporter, sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
clientManager users.ClientManager,
eventListener listener.Listener, eventListener listener.Listener,
) *storeFactory { ) *storeFactory {
return &storeFactory{ return &storeFactory{
cache: cache, cache: cache,
sentryReporter: sentryReporter, sentryReporter: sentryReporter,
panicHandler: panicHandler, panicHandler: panicHandler,
clientManager: clientManager,
eventListener: eventListener, eventListener: eventListener,
storeCache: store.NewCache(cache.GetIMAPCachePath()), storeCache: store.NewCache(cache.GetIMAPCachePath()),
} }
@ -56,7 +53,7 @@ func newStoreFactory(
// New creates new store for given user. // New creates new store for given user.
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) { func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
storePath := getUserStorePath(f.cache.GetDBDir(), user.ID()) storePath := getUserStorePath(f.cache.GetDBDir(), user.ID())
return store.New(f.sentryReporter, f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache) return store.New(f.sentryReporter, f.panicHandler, user, f.eventListener, storePath, f.storeCache)
} }
// Remove removes all store files for given user. // Remove removes all store files for given user.

View File

@ -25,8 +25,20 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
) )
// IsCatalinaOrNewer checks whether host is MacOS Catalina 10.15.x or higher. // IsCatalinaOrNewer checks whether the host is MacOS Catalina 10.15.x or higher.
func IsCatalinaOrNewer() bool { func IsCatalinaOrNewer() bool {
return isThisDarwinNewerOrEqual(getMinCatalina())
}
// IsBigSurOrNewer checks whether the host is MacOS BigSur 10.16.x or higher.
func IsBigSurOrNewer() bool {
return isThisDarwinNewerOrEqual(getMinBigSur())
}
func getMinCatalina() *semver.Version { return semver.MustParse("10.15.0") }
func getMinBigSur() *semver.Version { return semver.MustParse("10.16.0") }
func isThisDarwinNewerOrEqual(minVersion *semver.Version) bool {
if runtime.GOOS != "darwin" { if runtime.GOOS != "darwin" {
return false return false
} }
@ -36,16 +48,14 @@ func IsCatalinaOrNewer() bool {
return false return false
} }
return isVersionCatalinaOrNewer(strings.TrimSpace(string(rawVersion))) return isVersionEqualOrNewer(minVersion, strings.TrimSpace(string(rawVersion)))
} }
func isVersionCatalinaOrNewer(rawVersion string) bool { // isVersionEqualOrNewer is separated to be able to run test on other than darwin.
func isVersionEqualOrNewer(minVersion *semver.Version, rawVersion string) bool {
semVersion, err := semver.NewVersion(rawVersion) semVersion, err := semver.NewVersion(rawVersion)
if err != nil { if err != nil {
return false return false
} }
minVersion := semver.MustParse("10.15.0")
return semVersion.GreaterThan(minVersion) || semVersion.Equal(minVersion) return semVersion.GreaterThan(minVersion) || semVersion.Equal(minVersion)
} }

View File

@ -38,7 +38,27 @@ func TestIsVersionCatalinaOrNewer(t *testing.T) {
} }
for args, exp := range testData { for args, exp := range testData {
got := isVersionCatalinaOrNewer(args.version) got := isVersionEqualOrNewer(getMinCatalina(), args.version)
assert.Equal(t, exp, got, "version %v", args.version)
}
}
func TestIsVersionBigSurOrNewer(t *testing.T) {
testData := map[struct{ version string }]bool{
{""}: false,
{"9.0.0"}: false,
{"9.15.0"}: false,
{"10.13.0"}: false,
{"10.14.0"}: false,
{"10.14.99"}: false,
{"10.15.0"}: false,
{"10.16.0"}: true,
{"11.0.0"}: true,
{"11.1"}: true,
}
for args, exp := range testData {
got := isVersionEqualOrNewer(getMinBigSur(), args.version)
assert.Equal(t, exp, got, "version %v", args.version) assert.Equal(t, exp, got, "version %v", args.version)
} }
} }

View File

@ -29,10 +29,15 @@ import (
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/mobileconfig" "github.com/ProtonMail/proton-bridge/pkg/mobileconfig"
) )
const (
bigSurPreferncesPane = "/System/Library/PreferencePanes/Profiles.prefPane"
)
func init() { //nolint[gochecknoinit] func init() { //nolint[gochecknoinit]
available = append(available, &appleMail{}) available = append(available, &appleMail{})
} }
@ -43,7 +48,22 @@ func (c *appleMail) Name() string {
return "Apple Mail" return "Apple Mail"
} }
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) error { //nolint[funlen] func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) error {
mc := prepareMobileConfig(imapPort, smtpPort, imapSSL, smtpSSL, user, addressIndex)
confPath, err := saveConfigTemporarily(mc)
if err != nil {
return err
}
if useragent.IsBigSurOrNewer() {
return exec.Command("open", bigSurPreferncesPane, confPath).Run() //nolint[gosec] G204: open command is safe, mobileconfig is generated by us
}
return exec.Command("open", confPath).Run() //nolint[gosec] G204: open command is safe, mobileconfig is generated by us
}
func prepareMobileConfig(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) *mobileconfig.Config {
var addresses string var addresses string
var displayName string var displayName string
@ -62,7 +82,7 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
timestamp := strconv.FormatInt(time.Now().Unix(), 10) timestamp := strconv.FormatInt(time.Now().Unix(), 10)
mc := &mobileconfig.Config{ return &mobileconfig.Config{
EmailAddress: addresses, EmailAddress: addresses,
DisplayName: displayName, DisplayName: displayName,
Identifier: "protonmail " + displayName + timestamp, Identifier: "protonmail " + displayName + timestamp,
@ -80,10 +100,12 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
Username: displayName, Username: displayName,
}, },
} }
}
func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
dir, err := ioutil.TempDir("", "protonmail-autoconfig") dir, err := ioutil.TempDir("", "protonmail-autoconfig")
if err != nil { if err != nil {
return err return
} }
// Make sure the temporary file is deleted. // Make sure the temporary file is deleted.
@ -93,16 +115,17 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
})() })()
// Make sure the file is only readable for the current user. // Make sure the file is only readable for the current user.
f, err := os.OpenFile(filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig")), os.O_RDWR|os.O_CREATE, 0600) fname = filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig"))
f, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE, 0600)
if err != nil { if err != nil {
return err return
} }
if err := mc.WriteOut(f); err != nil { if err = mc.WriteOut(f); err != nil {
_ = f.Close() _ = f.Close()
return err return
} }
_ = f.Close() _ = f.Close()
return exec.Command("open", f.Name()).Run() // nolint[gosec] return
} }

View File

@ -18,6 +18,7 @@
package cliie package cliie
import ( import (
"context"
"strings" "strings"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
@ -67,7 +68,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
} }
f.Println("Authenticating ... ") f.Println("Authenticating ... ")
client, auth, err := f.ie.Login(loginName, password) client, auth, err := f.ie.Login(loginName, []byte(password))
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return
@ -79,7 +80,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return return
} }
err = client.Auth2FA(twoFactor, auth) err = client.Auth2FA(context.Background(), twoFactor)
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return
@ -95,7 +96,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
} }
f.Println("Adding account ...") f.Println("Adding account ...")
user, err := f.ie.FinishLogin(client, auth, mailboxPassword) user, err := f.ie.FinishLogin(client, auth, []byte(mailboxPassword))
if err != nil { if err != nil {
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful") log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
f.Println("Adding account was unsuccessful:", err) f.Println("Adding account was unsuccessful:", err)

View File

@ -84,11 +84,6 @@ func New( //nolint[funlen]
Aliases: []string{"u", "version", "v"}, Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates, Func: fe.checkUpdates,
}) })
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"},
Func: fe.checkInternetConnection,
})
fe.AddCmd(checkCmd) fe.AddCmd(checkCmd)
// Print info commands. // Print info commands.
@ -177,13 +172,13 @@ func New( //nolint[funlen]
} }
func (f *frontendCLI) watchEvents() { func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent) errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent) credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent) internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent) internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent) addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent) logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue) certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -208,13 +203,6 @@ func (f *frontendCLI) watchEvents() {
} }
} }
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
f.eventListener.RetryEmit(event)
return ch
}
// Loop starts the frontend loop with an interactive shell. // Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop() error { func (f *frontendCLI) Loop() error {
f.Print(` f.Print(`

View File

@ -29,14 +29,6 @@ func (f *frontendCLI) restart(c *ishell.Context) {
} }
} }
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.ie.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check your internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) { func (f *frontendCLI) printLogDir(c *ishell.Context) {
if path, err := f.locations.ProvideLogsPath(); err != nil { if path, err := f.locations.ProvideLogsPath(); err != nil {
f.Println("Failed to determine location of log files") f.Println("Failed to determine location of log files")

View File

@ -20,7 +20,7 @@ package cliie
import ( import (
"strings" "strings"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/fatih/color" "github.com/fatih/color"
) )
@ -71,7 +71,7 @@ func (f *frontendCLI) printAndLogError(args ...interface{}) {
func (f *frontendCLI) processAPIError(err error) { func (f *frontendCLI) processAPIError(err error) {
log.Warn("API error: ", err) log.Warn("API error: ", err)
switch err { switch err {
case pmapi.ErrAPINotReachable: case pmapi.ErrNoConnection:
f.notifyInternetOff() f.notifyInternetOff()
case pmapi.ErrUpgradeApplication: case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade() f.notifyNeedUpgrade()

View File

@ -18,6 +18,7 @@
package cli package cli
import ( import (
"context"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
@ -114,7 +115,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
} }
f.Println("Authenticating ... ") f.Println("Authenticating ... ")
client, auth, err := f.bridge.Login(loginName, password) client, auth, err := f.bridge.Login(loginName, []byte(password))
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return
@ -126,7 +127,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return return
} }
err = client.Auth2FA(twoFactor, auth) err = client.Auth2FA(context.Background(), twoFactor)
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return
@ -142,7 +143,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
} }
f.Println("Adding account ...") f.Println("Adding account ...")
user, err := f.bridge.FinishLogin(client, auth, mailboxPassword) user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
if err != nil { if err != nil {
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful") log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
f.Println("Adding account was unsuccessful:", err) f.Println("Adding account was unsuccessful:", err)

View File

@ -157,15 +157,6 @@ func New( //nolint[funlen]
}) })
fe.AddCmd(updatesCmd) fe.AddCmd(updatesCmd)
// Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"},
Func: fe.checkInternetConnection,
})
fe.AddCmd(checkCmd)
// Print info commands. // Print info commands.
fe.AddCmd(&ishell.Cmd{Name: "log-dir", fe.AddCmd(&ishell.Cmd{Name: "log-dir",
Help: "print path to directory with logs. (aliases: log, logs)", Help: "print path to directory with logs. (aliases: log, logs)",
@ -228,14 +219,14 @@ func New( //nolint[funlen]
} }
func (f *frontendCLI) watchEvents() { func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent) errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent) credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent) internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent) internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
addressChangedCh := f.getEventChannel(events.AddressChangedEvent) addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent) addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent) logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue) certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -262,13 +253,6 @@ func (f *frontendCLI) watchEvents() {
} }
} }
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
f.eventListener.RetryEmit(event)
return ch
}
// Loop starts the frontend loop with an interactive shell. // Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop() error { func (f *frontendCLI) Loop() error {
f.Print(` f.Print(`

View File

@ -39,14 +39,6 @@ func (f *frontendCLI) restart(c *ishell.Context) {
} }
} }
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.bridge.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check your internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) { func (f *frontendCLI) printLogDir(c *ishell.Context) {
if path, err := f.locations.ProvideLogsPath(); err != nil { if path, err := f.locations.ProvideLogsPath(); err != nil {
f.Println("Failed to determine location of log files") f.Println("Failed to determine location of log files")

View File

@ -71,7 +71,7 @@ func (f *frontendCLI) printAndLogError(args ...interface{}) {
func (f *frontendCLI) processAPIError(err error) { func (f *frontendCLI) processAPIError(err error) {
log.Warn("API error: ", err) log.Warn("API error: ", err)
switch err { switch err {
case pmapi.ErrAPINotReachable: case pmapi.ErrNoConnection:
f.notifyInternetOff() f.notifyInternetOff()
case pmapi.ErrUpgradeApplication: case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade() f.notifyNeedUpgrade()

View File

@ -94,7 +94,7 @@ Item {
font.pointSize : Style.main.fontSize * Style.pt font.pointSize : Style.main.fontSize * Style.pt
text: text:
"ProtonMail Bridge "+go.getBackendVersion()+"\n"+ "ProtonMail Bridge "+go.getBackendVersion()+"\n"+
"© 2020 Proton Technologies AG" "© 2021 Proton Technologies AG"
} }
} }
Row { Row {

View File

@ -409,7 +409,6 @@ Dialog {
onShow: { onShow: {
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.checkInternet()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.notifyError(gui.enums.errNoInternet) go.notifyError(gui.enums.errNoInternet)
root.hide() root.hide()

View File

@ -857,14 +857,12 @@ Dialog {
inputPort . checkIsANumber() inputPort . checkIsANumber()
//emailProvider . currentIndex!=0 //emailProvider . currentIndex!=0
)) isOK = false )) isOK = false
go.checkInternet()
if (winMain.updateState == gui.enums.statusNoInternet) { // todo: use main error dialog for this if (winMain.updateState == gui.enums.statusNoInternet) { // todo: use main error dialog for this
errorPopup.show(qsTr("Please check your internet connection.")) errorPopup.show(qsTr("Please check your internet connection."))
return false return false
} }
break break
case 2: // loading structure case 2: // loading structure
go.checkInternet()
if (winMain.updateState == gui.enums.statusNoInternet) { if (winMain.updateState == gui.enums.statusNoInternet) {
errorPopup.show(qsTr("Please check your internet connection.")) errorPopup.show(qsTr("Please check your internet connection."))
return false return false
@ -949,7 +947,6 @@ Dialog {
onShow : { onShow : {
root.clear() root.clear()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.checkInternet()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
winMain.popupMessage.show(go.canNotReachAPI) winMain.popupMessage.show(go.canNotReachAPI)
root.hide() root.hide()

View File

@ -81,7 +81,7 @@ Item {
color: Style.main.textDisabled color: Style.main.textDisabled
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
font.pointSize : Style.main.fontSize * Style.pt font.pointSize : Style.main.fontSize * Style.pt
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n© 2020 Proton Technologies AG" text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n© 2021 Proton Technologies AG"
} }
} }

View File

@ -25,33 +25,12 @@ import ProtonUI 1.0
Rectangle { Rectangle {
id: root id: root
property var iTry: 0 property var iTry: 0
property var secLeft: 0
property var second: 1000 // convert millisecond to second property var second: 1000 // convert millisecond to second
property var checkInterval: [ 5, 10, 30, 60, 120, 300, 600 ] // seconds
property bool isVisible: true property bool isVisible: true
property var fontSize : 1.2 * Style.main.fontSize property var fontSize : 1.2 * Style.main.fontSize
color : "black" color : "black"
state: "upToDate" state: "upToDate"
Timer {
id: retryInternet
interval: second
triggeredOnStart: false
repeat: true
onTriggered : {
secLeft--
if (secLeft <= 0) {
retryInternet.stop()
go.checkInternet()
if (iTry < checkInterval.length-1) {
iTry++
}
secLeft=checkInterval[iTry]
retryInternet.start()
}
}
}
Row { Row {
id: messageRow id: messageRow
anchors.centerIn: root anchors.centerIn: root
@ -110,16 +89,12 @@ Rectangle {
case "internetCheck": case "internetCheck":
break; break;
case "noInternet" : case "noInternet" :
retryInternet.start()
secLeft=checkInterval[iTry]
break; break;
case "oldVersion": case "oldVersion":
break; break;
case "forceUpdate": case "forceUpdate":
break; break;
case "upToDate": case "upToDate":
iTry = 0
secLeft=checkInterval[iTry]
break; break;
case "updateRestart": case "updateRestart":
break; break;
@ -128,24 +103,6 @@ Rectangle {
default : default :
break; break;
} }
if (root.state!="noInternet") {
retryInternet.stop()
}
}
function timeToRetry() {
if (secLeft==1){
return qsTr("a second", "time to wait till internet connection is retried")
} else if (secLeft<60){
return secLeft + " " + qsTr("seconds", "time to wait till internet connection is retried")
} else {
var leading = ""+secLeft%60
if (leading.length < 2) {
leading = "0" + leading
}
return Math.floor(secLeft/60) + ":" + leading
}
} }
states: [ states: [
@ -194,23 +151,15 @@ Rectangle {
PropertyChanges { PropertyChanges {
target: message target: message
color: Style.main.line color: Style.main.line
text: qsTr("Cannot contact server. Retrying in ", "displayed when the app is disconnected from the internet or server has problems")+timeToRetry()+"." text: qsTr("Cannot contact server. Please wait...", "displayed when the app is disconnected from the internet or server has problems")
} }
PropertyChanges { PropertyChanges {
target: linkText target: linkText
visible: false visible: false
} }
PropertyChanges {
target: actionText
visible: true
text: qsTr("Retry now", "click to try to connect to the internet when the app is disconnected from the internet")
onClicked: {
go.checkInternet()
}
}
PropertyChanges { PropertyChanges {
target: separatorText target: separatorText
visible: true visible: false
text: "|" text: "|"
} }
PropertyChanges { PropertyChanges {

View File

@ -1331,10 +1331,6 @@ Window {
return (fname!="fail") return (fname!="fail")
} }
function checkInternet() {
// nothing to do
}
function loadImportReports(fname) { function loadImportReports(fname) {
console.log("load import reports for ", fname) console.log("load import reports for ", fname)
} }

View File

@ -20,6 +20,7 @@
package qtcommon package qtcommon
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@ -164,7 +165,7 @@ func (a *Accounts) showLoginError(err error, scope string) bool {
return false return false
} }
log.Warnf("%s: %v", scope, err) log.Warnf("%s: %v", scope, err)
if err == pmapi.ErrAPINotReachable { if err == pmapi.ErrNoConnection {
a.qml.SetConnectionStatus(false) a.qml.SetConnectionStatus(false)
SendNotification(a.qml, TabAccount, a.qml.CanNotReachAPI()) SendNotification(a.qml, TabAccount, a.qml.CanNotReachAPI())
a.qml.ProcessFinished() a.qml.ProcessFinished()
@ -185,7 +186,7 @@ func (a *Accounts) showLoginError(err error, scope string) bool {
// 2: when has no 2FA but have MBOX // 2: when has no 2FA but have MBOX
func (a *Accounts) Login(login, password string) int { func (a *Accounts) Login(login, password string) int {
var err error var err error
a.authClient, a.auth, err = a.um.Login(login, password) a.authClient, a.auth, err = a.um.Login(login, []byte(password))
if a.showLoginError(err, "login") { if a.showLoginError(err, "login") {
return -1 return -1
} }
@ -207,7 +208,7 @@ func (a *Accounts) Auth2FA(twoFacAuth string) int {
if a.auth == nil || a.authClient == nil { if a.auth == nil || a.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient) err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient)
} else { } else {
err = a.authClient.Auth2FA(twoFacAuth, a.auth) err = a.authClient.Auth2FA(context.Background(), twoFacAuth)
} }
if a.showLoginError(err, "auth2FA") { if a.showLoginError(err, "auth2FA") {
@ -229,7 +230,7 @@ func (a *Accounts) AddAccount(mailboxPassword string) int {
return -1 return -1
} }
user, err := a.um.FinishLogin(a.authClient, a.auth, mailboxPassword) user, err := a.um.FinishLogin(a.authClient, a.auth, []byte(mailboxPassword))
if err != nil { if err != nil {
log.WithError(err).Error("Login was unsuccessful") log.WithError(err).Error("Login was unsuccessful")
a.qml.SetAddAccountWarning("Failure: "+err.Error(), -2) a.qml.SetAddAccountWarning("Failure: "+err.Error(), -2)

View File

@ -113,10 +113,3 @@ type Listener interface {
Add(string, chan<- string) Add(string, chan<- string)
RetryEmit(string) RetryEmit(string)
} }
func MakeAndRegisterEvent(eventListener Listener, event string) <-chan string {
ch := make(chan string)
eventListener.Add(event, ch)
eventListener.RetryEmit(event)
return ch
}

View File

@ -143,16 +143,16 @@ func (f *FrontendQt) NotifySilentUpdateError(err error) {
} }
func (f *FrontendQt) watchEvents() { func (f *FrontendQt) watchEvents() {
credentialsErrorCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.CredentialsErrorEvent) credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOffEvent) internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOnEvent) internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
secondInstanceCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.SecondInstanceEvent) secondInstanceCh := f.eventListener.ProvideChannel(events.SecondInstanceEvent)
restartBridgeCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.RestartBridgeEvent) restartBridgeCh := f.eventListener.ProvideChannel(events.RestartBridgeEvent)
addressChangedCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedEvent) addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedLogoutEvent) addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.LogoutEvent) logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
updateApplicationCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UpgradeApplicationEvent) updateApplicationCh := f.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
newUserCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UserRefreshEvent) newUserCh := f.eventListener.ProvideChannel(events.UserRefreshEvent)
for { for {
select { select {
case <-credentialsErrorCh: case <-credentialsErrorCh:
@ -351,11 +351,6 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
// } // }
//} //}
// checkInternet is almost idetical to bridge
func (f *FrontendQt) checkInternet() {
f.Qml.SetConnectionStatus(f.ie.CheckConnection() == nil)
}
func (f *FrontendQt) showError(code int, err error) { func (f *FrontendQt) showError(code int, err error) {
f.Qml.SetErrorDescription(err.Error()) f.Qml.SetErrorDescription(err.Error())
log.WithField("code", code).Errorln(err.Error()) log.WithField("code", code).Errorln(err.Error())

View File

@ -78,7 +78,6 @@ type GoQMLInterface struct {
_ string `property:"versionCheckFailed"` _ string `property:"versionCheckFailed"`
// //
_ func(isAvailable bool) `signal:"setConnectionStatus"` _ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`
@ -189,8 +188,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
return f.programVersion return f.programVersion
}) })
s.ConnectCheckInternet(f.checkInternet)
s.ConnectSetToRestart(f.restarter.SetToRestart) s.ConnectSetToRestart(f.restarter.SetToRestart)
s.ConnectLoadStructureForExport(f.LoadStructureForExport) s.ConnectLoadStructureForExport(f.LoadStructureForExport)

View File

@ -20,6 +20,7 @@
package qt package qt
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
@ -130,7 +131,7 @@ func (s *FrontendQt) showLoginError(err error, scope string) bool {
return false return false
} }
log.Warnf("%s: %v", scope, err) log.Warnf("%s: %v", scope, err)
if err == pmapi.ErrAPINotReachable { if err == pmapi.ErrNoConnection {
s.Qml.SetConnectionStatus(false) s.Qml.SetConnectionStatus(false)
s.SendNotification(TabAccount, s.Qml.CanNotReachAPI()) s.SendNotification(TabAccount, s.Qml.CanNotReachAPI())
s.Qml.ProcessFinished() s.Qml.ProcessFinished()
@ -151,7 +152,7 @@ func (s *FrontendQt) showLoginError(err error, scope string) bool {
// 2: when has no 2FA but have MBOX // 2: when has no 2FA but have MBOX
func (s *FrontendQt) login(login, password string) int { func (s *FrontendQt) login(login, password string) int {
var err error var err error
s.authClient, s.auth, err = s.bridge.Login(login, password) s.authClient, s.auth, err = s.bridge.Login(login, []byte(password))
if s.showLoginError(err, "login") { if s.showLoginError(err, "login") {
return -1 return -1
} }
@ -173,7 +174,7 @@ func (s *FrontendQt) auth2FA(twoFacAuth string) int {
if s.auth == nil || s.authClient == nil { if s.auth == nil || s.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient) err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient)
} else { } else {
err = s.authClient.Auth2FA(twoFacAuth, s.auth) err = s.authClient.Auth2FA(context.Background(), twoFacAuth)
} }
if s.showLoginError(err, "auth2FA") { if s.showLoginError(err, "auth2FA") {
@ -194,7 +195,7 @@ func (s *FrontendQt) addAccount(mailboxPassword string) int {
return -1 return -1
} }
user, err := s.bridge.FinishLogin(s.authClient, s.auth, mailboxPassword) user, err := s.bridge.FinishLogin(s.authClient, s.auth, []byte(mailboxPassword))
if err != nil { if err != nil {
log.WithError(err).Error("Login was unsuccessful") log.WithError(err).Error("Login was unsuccessful")
s.Qml.SetAddAccountWarning("Failure: "+err.Error(), -2) s.Qml.SetAddAccountWarning("Failure: "+err.Error(), -2)

View File

@ -191,20 +191,20 @@ func (s *FrontendQt) NotifySilentUpdateError(err error) {
func (s *FrontendQt) watchEvents() { func (s *FrontendQt) watchEvents() {
s.WaitUntilFrontendIsReady() s.WaitUntilFrontendIsReady()
errorCh := s.getEventChannel(events.ErrorEvent) errorCh := s.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := s.getEventChannel(events.CredentialsErrorEvent) credentialsErrorCh := s.eventListener.ProvideChannel(events.CredentialsErrorEvent)
outgoingNoEncCh := s.getEventChannel(events.OutgoingNoEncEvent) outgoingNoEncCh := s.eventListener.ProvideChannel(events.OutgoingNoEncEvent)
noActiveKeyForRecipientCh := s.getEventChannel(events.NoActiveKeyForRecipientEvent) noActiveKeyForRecipientCh := s.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent)
internetOffCh := s.getEventChannel(events.InternetOffEvent) internetOffCh := s.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := s.getEventChannel(events.InternetOnEvent) internetOnCh := s.eventListener.ProvideChannel(events.InternetOnEvent)
secondInstanceCh := s.getEventChannel(events.SecondInstanceEvent) secondInstanceCh := s.eventListener.ProvideChannel(events.SecondInstanceEvent)
restartBridgeCh := s.getEventChannel(events.RestartBridgeEvent) restartBridgeCh := s.eventListener.ProvideChannel(events.RestartBridgeEvent)
addressChangedCh := s.getEventChannel(events.AddressChangedEvent) addressChangedCh := s.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := s.getEventChannel(events.AddressChangedLogoutEvent) addressChangedLogoutCh := s.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := s.getEventChannel(events.LogoutEvent) logoutCh := s.eventListener.ProvideChannel(events.LogoutEvent)
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent) updateApplicationCh := s.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
newUserCh := s.getEventChannel(events.UserRefreshEvent) newUserCh := s.eventListener.ProvideChannel(events.UserRefreshEvent)
certIssue := s.getEventChannel(events.TLSCertIssue) certIssue := s.eventListener.ProvideChannel(events.TLSCertIssue)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -254,13 +254,6 @@ func (s *FrontendQt) watchEvents() {
} }
} }
func (s *FrontendQt) getEventChannel(event string) <-chan string {
ch := make(chan string)
s.eventListener.Add(event, ch)
s.eventListener.RetryEmit(event)
return ch
}
// Loop function for tests. // Loop function for tests.
// //
// It runs QtExecute in new thread with function returning itself after setup. // It runs QtExecute in new thread with function returning itself after setup.
@ -653,10 +646,6 @@ func (s *FrontendQt) isSMTPSTARTTLS() bool {
return !s.settings.GetBool(settings.SMTPSSLKey) return !s.settings.GetBool(settings.SMTPSSLKey)
} }
func (s *FrontendQt) checkInternet() {
s.Qml.SetConnectionStatus(s.bridge.CheckConnection() == nil)
}
func (s *FrontendQt) switchAddressModeUser(iAccount int) { func (s *FrontendQt) switchAddressModeUser(iAccount int) {
defer s.Qml.ProcessFinished() defer s.Qml.ProcessFinished()
userID := s.Accounts.get(iAccount).UserID() userID := s.Accounts.get(iAccount).UserID()

View File

@ -84,7 +84,6 @@ type GoQMLInterface struct {
_ string `property:"progressDescription"` _ string `property:"progressDescription"`
_ func(isAvailable bool) `signal:"setConnectionStatus"` _ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`
@ -205,8 +204,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
return f.programVer return f.programVer
}) })
s.ConnectCheckInternet(f.checkInternet)
s.ConnectSetToRestart(f.restarter.SetToRestart) s.ConnectSetToRestart(f.restarter.SetToRestart)
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc) s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)

View File

@ -49,13 +49,12 @@ type Updater interface {
// UserManager is an interface of users needed by frontend. // UserManager is an interface of users needed by frontend.
type UserManager interface { type UserManager interface {
Login(username, password string) (pmapi.Client, *pmapi.Auth, error) Login(username string, password []byte) (pmapi.Client, *pmapi.Auth, error)
FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error)
GetUsers() []User GetUsers() []User
GetUser(query string) (User, error) GetUser(query string) (User, error)
DeleteUser(userID string, clearCache bool) error DeleteUser(userID string, clearCache bool) error
ClearData() error ClearData() error
CheckConnection() error
} }
// User is an interface of user needed by frontend. // User is an interface of user needed by frontend.
@ -95,7 +94,7 @@ func NewBridgeWrap(bridge *bridge.Bridge) *bridgeWrap { //nolint[golint]
return &bridgeWrap{Bridge: bridge} return &bridgeWrap{Bridge: bridge}
} }
func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) { func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) {
return b.Bridge.FinishLogin(client, auth, mailboxPassword) return b.Bridge.FinishLogin(client, auth, mailboxPassword)
} }
@ -135,7 +134,7 @@ func NewImportExportWrap(ie *importexport.ImportExport) *importExportWrap { //no
return &importExportWrap{ImportExport: ie} return &importExportWrap{ImportExport: ie}
} }
func (b *importExportWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) { func (b *importExportWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) {
return b.ImportExport.FinishLogin(client, auth, mailboxPassword) return b.ImportExport.FinishLogin(client, auth, mailboxPassword)
} }

View File

@ -38,11 +38,10 @@ type bridgeUser interface {
IsCombinedAddressMode() bool IsCombinedAddressMode() bool
GetAddressID(address string) (string, error) GetAddressID(address string) (string, error)
GetPrimaryAddress() string GetPrimaryAddress() string
UpdateUser() error
Logout() error Logout() error
CloseConnection(address string) CloseConnection(address string)
GetStore() storeUserProvider GetStore() storeUserProvider
GetTemporaryPMAPIClient() pmapi.Client GetClient() pmapi.Client
} }
type bridgeWrap struct { type bridgeWrap struct {

View File

@ -0,0 +1,85 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package idle
import (
"bufio"
"errors"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/server"
)
const (
idleCommand = "IDLE" // Capability and Command identificator
doneLine = "DONE"
)
// Handler for IDLE extension.
type Handler struct{}
// Command for IDLE handler.
func (h *Handler) Command() *imap.Command {
return &imap.Command{Name: idleCommand}
}
// Parse for IDLE handler.
func (h *Handler) Parse(fields []interface{}) error {
return nil
}
// Handle the IDLE request.
func (h *Handler) Handle(conn server.Conn) error {
cont := &imap.ContinuationReq{Info: "idling"}
if err := conn.WriteResp(cont); err != nil {
return err
}
// Wait for DONE
scanner := bufio.NewScanner(conn)
scanner.Scan()
if err := scanner.Err(); err != nil {
return err
}
if strings.ToUpper(scanner.Text()) != doneLine {
return errors.New("expected DONE")
}
return nil
}
type extension struct{}
func (ext *extension) Capabilities(c server.Conn) []string {
return []string{idleCommand}
}
func (ext *extension) Command(name string) server.HandlerFactory {
if name != idleCommand {
return nil
}
return func() server.Handler {
return &Handler{}
}
}
func NewExtension() server.Extension {
return &extension{}
}

View File

@ -0,0 +1,269 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"net/mail"
"strings"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/textproto"
"github.com/pkg/errors"
)
// CreateMessage appends a new message to this mailbox. The \Recent flag will
// be added regardless of whether flags is empty or not. If date is nil, the
// current time will be used.
//
// If the Backend implements Updater, it must notify the client immediately
// via a mailbox update.
func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
return im.logCommand(func() error {
return im.createMessage(flags, date, body)
}, "APPEND", flags, date)
}
func (im *imapMailbox) createMessage(imapFlags []string, date time.Time, r imap.Literal) error { //nolint[funlen]
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
// NOTE: Is this lock meant to be here?
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
body, err := ioutil.ReadAll(r)
if err != nil {
return err
}
addr := im.storeAddress.APIAddress()
if addr == nil {
return errors.New("no available address for encryption")
}
kr, err := im.user.client().KeyRingForAddressID(addr.ID)
if err != nil {
return err
}
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
return im.createDraftMessage(kr, addr.Email, body)
}
if im.storeMailbox.LabelID() == pmapi.SentLabel {
m, _, _, _, err := message.Parse(bytes.NewReader(body))
if err != nil {
return err
}
if m.Sender == nil {
m.Sender = &mail.Address{Address: addr.Email}
}
if user, err := im.user.backend.bridge.GetUser(pmapi.SanitizeEmail(m.Sender.Address)); err == nil && user.ID() == im.storeUser.UserID() {
logEntry := im.log.WithField("sender", m.Sender).WithField("extID", m.Header.Get("Message-Id")).WithField("date", date)
if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) {
logEntry.Info("Ignoring APPEND of duplicate to Sent folder")
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID})
}
logEntry.Info("No matching UID, continuing APPEND to Sent")
}
}
hdr, err := textproto.ReadHeader(bufio.NewReader(bytes.NewReader(body)))
if err != nil {
return err
}
// Avoid appending a message which is already on the server. Apply the new label instead.
// This always happens with Outlook because it uses APPEND instead of COPY.
internalID := hdr.Get("X-Pm-Internal-Id")
// In case there is a mail client which corrupts headers, try "References" too.
if internalID == "" {
if references := strings.Fields(hdr.Get("References")); len(references) > 0 {
if match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(references[len(references)-1]); len(match) == 2 {
internalID = match[1]
}
}
}
if internalID != "" {
if msg, err := im.storeMailbox.GetMessage(internalID); err == nil {
if im.user.user.IsCombinedAddressMode() || im.storeAddress.AddressID() == msg.Message().AddressID {
return im.labelExistingMessage(msg)
}
}
}
return im.importMessage(kr, hdr, body, imapFlags, date)
}
func (im *imapMailbox) createDraftMessage(kr *crypto.KeyRing, email string, body []byte) error {
im.log.Info("Creating draft message")
m, _, _, readers, err := message.Parse(bytes.NewReader(body))
if err != nil {
return err
}
if m.Sender == nil {
m.Sender = &mail.Address{}
}
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, email)
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
if err != nil {
return errors.Wrap(err, "failed to create draft")
}
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{draft.ID}))
}
func findMailboxForAddress(address storeAddressProvider, labelID string) (storeMailboxProvider, error) {
for _, mailBox := range address.ListMailboxes() {
if mailBox.LabelID() == labelID {
return mailBox, nil
}
}
return nil, fmt.Errorf("could not find %v label in mailbox for user %v", labelID,
address.AddressString())
}
func (im *imapMailbox) labelExistingMessage(msg storeMessageProvider) error { //nolint[funlen]
im.log.Info("Labelling existing message")
// IMAP clients can move message to local folder (setting \Deleted flag)
// and then move it back (IMAP client does not remember the message,
// so instead removing the flag it imports duplicate message).
// Regular IMAP server would keep the message twice and later EXPUNGE would
// not delete the message (EXPUNGE would delete the original message and
// the new duplicate one would stay). API detects duplicates; therefore
// we need to remove \Deleted flag if IMAP client re-imports.
if msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted([]string{msg.ID()}); err != nil {
log.WithError(err).Error("Failed to undelete re-imported message")
}
}
// Outlook Uses APPEND instead of COPY. There is no need to copy to All Mail because messages are already there.
// If the message is copied from Spam or Trash, it must be moved otherwise we will have data loss.
// If the message is moved from any folder, the moment when expunge happens on source we will move message trash unless we move it to archive.
// If the message is already in Archive we should not call API at all.
// Otherwise the message is already in All mail, Return OK.
var storeMBox = im.storeMailbox
if pmapi.AllMailLabel == storeMBox.LabelID() {
if msg.Message().HasLabelID(pmapi.ArchiveLabel) {
return uidplus.AppendResponse(storeMBox.UIDValidity(), storeMBox.GetUIDList([]string{msg.ID()}))
}
var err error
storeMBox, err = findMailboxForAddress(im.storeAddress, pmapi.ArchiveLabel)
if err != nil {
return err
}
}
if err := storeMBox.LabelMessages([]string{msg.ID()}); err != nil {
return err
}
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{msg.ID()}))
}
func (im *imapMailbox) importMessage(kr *crypto.KeyRing, hdr textproto.Header, body []byte, imapFlags []string, date time.Time) error { //nolint[funlen]
im.log.Info("Importing external message")
var (
seen bool
flags int64
labelIDs []string
time int64
)
if hdr.Get("received") == "" {
flags = pmapi.FlagSent
} else {
flags = pmapi.FlagReceived
}
for _, flag := range imapFlags {
switch flag {
case imap.DraftFlag:
flags &= ^pmapi.FlagSent
flags &= ^pmapi.FlagReceived
case imap.SeenFlag:
seen = true
case imap.FlaggedFlag:
labelIDs = append(labelIDs, pmapi.StarredLabel)
case imap.AnsweredFlag:
flags |= pmapi.FlagReplied
}
}
if !date.IsZero() {
time = date.Unix()
}
enc, err := message.EncryptRFC822(kr, bytes.NewReader(body))
if err != nil {
return err
}
var targetMailbox = im.storeMailbox
if targetMailbox.LabelID() == pmapi.AllMailLabel {
// Importing mail in directly into All Mail is not allowed. Instead we redirect the import to Archive
// The mail will automatically appear in All mail. The appends response still reports that the mail was
// successfully APPEND to All Mail.
targetMailbox, err = findMailboxForAddress(im.storeAddress, pmapi.ArchiveLabel)
if err != nil {
return err
}
}
messageID, err := targetMailbox.ImportMessage(enc, seen, labelIDs, flags, time)
if err != nil {
return err
}
msg, err := targetMailbox.GetMessage(messageID)
if err != nil {
return err
}
if msg.IsMarkedDeleted() {
if err := targetMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil {
log.WithError(err).Error("Failed to undelete re-imported message")
}
}
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID}))
}

View File

@ -0,0 +1,322 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"bytes"
"context"
"github.com/ProtonMail/proton-bridge/internal/imap/cache"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func (im *imapMailbox) getMessage(
storeMessage storeMessageProvider,
items []imap.FetchItem,
msgBuildCountHistogram *msgBuildCountHistogram,
) (msg *imap.Message, err error) {
msglog := im.log.WithField("msgID", storeMessage.ID())
msglog.Trace("Getting message")
seqNum, err := storeMessage.SequenceNumber()
if err != nil {
return
}
m := storeMessage.Message()
msg = imap.NewMessage(seqNum, items)
for _, item := range items {
switch item {
case imap.FetchEnvelope:
// No need to check IsFullHeaderCached here. API header
// contain enough information to build the envelope.
msg.Envelope = message.GetEnvelope(m, storeMessage.GetMIMEHeader())
case imap.FetchBody, imap.FetchBodyStructure:
structure, err := im.getBodyStructure(storeMessage)
if err != nil {
return nil, err
}
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
return nil, err
}
case imap.FetchFlags:
msg.Flags = message.GetFlags(m)
if storeMessage.IsMarkedDeleted() {
msg.Flags = append(msg.Flags, imap.DeletedFlag)
}
case imap.FetchInternalDate:
// Apple Mail crashes fetching messages with date older than 1970.
// There is no point having message older than RFC itself, it's not possible.
msg.InternalDate = message.SanitizeMessageDate(m.Time)
case imap.FetchRFC822Size:
if msg.Size, err = im.getSize(storeMessage); err != nil {
return nil, err
}
case imap.FetchUid:
if msg.Uid, err = storeMessage.UID(); err != nil {
return nil, err
}
case imap.FetchAll, imap.FetchFast, imap.FetchFull, imap.FetchRFC822, imap.FetchRFC822Header, imap.FetchRFC822Text:
fallthrough // this is list of defined items by go-imap, but items can be also sections generated from requests
default:
if err = im.getLiteralForSection(item, msg, storeMessage, msgBuildCountHistogram); err != nil {
return
}
}
}
return msg, err
}
// getSize returns cached size or it will build the message, save the size in
// DB and then returns the size after build.
//
// We are storing size in DB as part of pmapi messages metada. The size
// attribute on the server represents size of encrypted body. The value is
// cleared in Bridge and the final decrypted size (including header, attachment
// and MIME structure) is computed after building the message.
func (im *imapMailbox) getSize(storeMessage storeMessageProvider) (uint32, error) {
m := storeMessage.Message()
if m.Size <= 0 {
im.log.WithField("msgID", m.ID).Debug("Size unknown - downloading body")
// We are sure the size is not a problem right now. Clients
// might not first check sizes of all messages so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude getting size from the
// counting and see build count as real message build.
if _, _, err := im.getBodyAndStructure(storeMessage, nil); err != nil {
return 0, err
}
}
return uint32(m.Size), nil
}
func (im *imapMailbox) getLiteralForSection(
itemSection imap.FetchItem,
msg *imap.Message,
storeMessage storeMessageProvider,
msgBuildCountHistogram *msgBuildCountHistogram,
) error {
section, err := imap.ParseBodySectionName(itemSection)
if err != nil {
log.WithError(err).Warn("Failed to parse body section name; part will be skipped")
return nil //nolint[nilerr] ignore error
}
var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section, msgBuildCountHistogram); err != nil {
return err
}
msg.Body[section] = literal
return nil
}
// getBodyStructure returns the cached body structure or it will build the message,
// save the structure in DB and then returns the structure after build.
//
// Apple Mail requests body structure for all messages irregularly. We cache
// bodystructure in local database in order to not re-download all messages
// from server.
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *message.BodyStructure, err error) {
bs, err = storeMessage.GetBodyStructure()
if err != nil {
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
}
if bs == nil {
// We are sure the body structure is not a problem right now.
// Clients might do first fetch body structure so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude first body structure fetch
// from the counting and see build count as real message build.
if bs, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
return
}
}
return
}
func (im *imapMailbox) getBodyAndStructure(
storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram,
) (
structure *message.BodyStructure, bodyReader *bytes.Reader, err error,
) {
m := storeMessage.Message()
id := im.storeUser.UserID() + m.ID
cache.BuildLock(id)
defer cache.BuildUnlock(id)
bodyReader, structure = cache.LoadMail(id)
// return the message which was found in cache
if bodyReader.Len() != 0 && structure != nil {
return structure, bodyReader, nil
}
structure, body, err := im.buildMessage(m)
bodyReader = bytes.NewReader(body)
size := int64(len(body))
l := im.log.WithField("newSize", size).WithField("msgID", m.ID)
if err != nil || structure == nil || size == 0 {
l.WithField("hasStructure", structure != nil).Warn("Failed to build message")
return structure, bodyReader, err
}
// Save the size, body structure and header even for messages which
// were unable to decrypt. Hence they doesn't have to be computed every
// time.
m.Size = size
cacheMessageInStore(storeMessage, structure, body, l)
if msgBuildCountHistogram != nil {
times, errCount := storeMessage.IncreaseBuildCount()
if errCount != nil {
l.WithError(errCount).Warn("Cannot increase build count")
}
msgBuildCountHistogram.add(times)
}
// Drafts can change therefore we don't want to cache them.
if !isMessageInDraftFolder(m) {
cache.SaveMail(id, body, structure)
}
return structure, bodyReader, err
}
func cacheMessageInStore(storeMessage storeMessageProvider, structure *message.BodyStructure, body []byte, l *logrus.Entry) {
m := storeMessage.Message()
if errSize := storeMessage.SetSize(m.Size); errSize != nil {
l.WithError(errSize).Warn("Cannot update size while building")
}
if structure != nil && !isMessageInDraftFolder(m) {
if errStruct := storeMessage.SetBodyStructure(structure); errStruct != nil {
l.WithError(errStruct).Warn("Cannot update bodystructure while building")
}
}
header, errHead := structure.GetMailHeaderBytes(bytes.NewReader(body))
if errHead == nil && len(header) != 0 {
if errStore := storeMessage.SetHeader(header); errStore != nil {
l.WithError(errStore).Warn("Cannot update header in store")
}
} else {
l.WithError(errHead).Warn("Cannot get header bytes from structure")
}
}
func isMessageInDraftFolder(m *pmapi.Message) bool {
for _, labelID := range m.LabelIDs {
if labelID == pmapi.DraftLabel {
return true
}
}
return false
}
// This will download message (or read from cache) and pick up the section,
// extract data (header,body, both) and trim the output if needed.
//
// In order to speed up (avoid download and decryptions) we
// cache the header. If a mail header was requested and DB
// contains full header (it means it was already built once)
// the DB header can be used without downloading and decrypting.
// Otherwise header is incomplete and clients would have issues
// e.g. AppleMail expects `text/plain` in HTML mails.
//
// For all other cases it is necessary to download and decrypt the message
// and drop the header which was obtained from cache. The header will
// will be stored in DB once successfully built. Check `getBodyAndStructure`.
func (im *imapMailbox) getMessageBodySection(
storeMessage storeMessageProvider,
section *imap.BodySectionName,
msgBuildCountHistogram *msgBuildCountHistogram,
) (imap.Literal, error) {
var header []byte
var response []byte
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body")
isMainHeaderRequested := len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier
if isMainHeaderRequested && storeMessage.IsFullHeaderCached() {
header = storeMessage.GetHeader()
} else {
structure, bodyReader, err := im.getBodyAndStructure(storeMessage, msgBuildCountHistogram)
if err != nil {
return nil, err
}
switch {
case section.Specifier == imap.EntireSpecifier && len(section.Path) == 0:
// An empty section specification refers to the entire message, including the header.
response, err = structure.GetSection(bodyReader, section.Path)
case section.Specifier == imap.TextSpecifier || (section.Specifier == imap.EntireSpecifier && len(section.Path) != 0):
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header.
// Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
response, err = structure.GetSectionContent(bodyReader, section.Path)
case section.Specifier == imap.MIMESpecifier: // The MIME part specifier refers to the [MIME-IMB] header for this part.
fallthrough
case section.Specifier == imap.HeaderSpecifier:
header, err = structure.GetSectionHeaderBytes(bodyReader, section.Path)
default:
err = errors.New("Unknown specifier " + string(section.Specifier))
}
if err != nil {
return nil, err
}
}
if header != nil {
response = filterHeader(header, section)
}
// Trim any output if requested.
return bytes.NewBuffer(section.ExtractPartial(response)), nil
}
// buildMessage from PM to IMAP.
func (im *imapMailbox) buildMessage(m *pmapi.Message) (*message.BodyStructure, []byte, error) {
body, err := im.builder.NewJobWithOptions(
context.Background(),
im.user.client(),
m.ID,
message.JobOptions{
IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead.
SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate.
AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id.
AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id.
AddMessageDate: true, // Whether to include message time as X-Pm-Date.
AddMessageIDReference: true, // Whether to include the MessageID in References.
},
).GetResult()
if err != nil {
return nil, nil, err
}
structure, err := message.NewBodyStructure(bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
return structure, body, nil
}

View File

@ -0,0 +1,67 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFilterHeader(t *testing.T) {
const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n\r\n"
assert.Equal(t, "To: somebody\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To")
})))
assert.Equal(t, "From: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "From")
})))
assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To") || strings.EqualFold(field, "From")
})))
assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "Subject")
})))
}
// TestFilterHeaderNoNewline tests that we don't include a trailing newline when filtering
// if the original header also lacks one (which it can legally do if there is no body).
func TestFilterHeaderNoNewline(t *testing.T) {
const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n"
assert.Equal(t, "To: somebody\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To")
})))
assert.Equal(t, "From: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "From")
})))
assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To") || strings.EqualFold(field, "From")
})))
assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "Subject")
})))
}

View File

@ -0,0 +1,71 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"bytes"
"strings"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/emersion/go-imap"
)
func filterHeader(header []byte, section *imap.BodySectionName) []byte {
// Empty section.Fields means BODY[HEADER] was requested so we should return the full header.
if len(section.Fields) == 0 {
return header
}
fieldMap := make(map[string]struct{})
for _, field := range section.Fields {
fieldMap[strings.ToLower(field)] = struct{}{}
}
return filterHeaderLines(header, func(field string) bool {
_, ok := fieldMap[strings.ToLower(field)]
if section.NotFields {
ok = !ok
}
return ok
})
}
func filterHeaderLines(header []byte, wantField func(string) bool) []byte {
var res []byte
for _, line := range message.HeaderLines(header) {
if len(bytes.TrimSpace(line)) == 0 {
res = append(res, line...)
} else {
split := bytes.SplitN(line, []byte(": "), 2)
if len(split) != 2 {
continue
}
if wantField(string(bytes.ToLower(split[0]))) {
res = append(res, line...)
}
}
}
return res
}

View File

@ -1,531 +0,0 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"bytes"
"context"
"fmt"
"io"
"net/mail"
"net/textproto"
"sort"
"strings"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/cache"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
)
var (
rfc822Birthday = time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC) //nolint[gochecknoglobals]
)
type doNotCacheError struct{ e error }
func (dnc *doNotCacheError) Error() string { return dnc.e.Error() }
func (dnc *doNotCacheError) add(err error) { dnc.e = multierror.Append(dnc.e, err) }
func (dnc *doNotCacheError) errorOrNil() error {
if dnc == nil {
return nil
}
if dnc.e != nil {
return dnc
}
return nil
}
// CreateMessage appends a new message to this mailbox. The \Recent flag will
// be added regardless of whether flags is empty or not. If date is nil, the
// current time will be used.
//
// If the Backend implements Updater, it must notify the client immediately
// via a mailbox update.
func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
return im.logCommand(func() error {
return im.createMessage(flags, date, body)
}, "APPEND", flags, date)
}
func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.Literal) error { // nolint[funlen]
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
m, _, _, readers, err := message.Parse(body)
if err != nil {
return err
}
addr := im.storeAddress.APIAddress()
if addr == nil {
return errors.New("no available address for encryption")
}
m.AddressID = addr.ID
kr, err := im.user.client().KeyRingForAddressID(addr.ID)
if err != nil {
return err
}
// Handle imported messages which have no "Sender" address.
// This sometimes occurs with outlook which reports errors as imported emails or for drafts.
if m.Sender == nil {
im.log.Warning("Append: Missing email sender. Will use main address")
m.Sender = &mail.Address{
Name: "",
Address: addr.Email,
}
}
// "Drafts" needs to call special API routes.
// Clients always append the whole message again and remove the old one.
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
// Sender address needs to be sanitised (drafts need to match cases exactly).
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, addr.Email)
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
if err != nil {
return errors.Wrap(err, "failed to create draft")
}
targetSeq := im.storeMailbox.GetUIDList([]string{draft.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
// We need to make sure this is an import, and not a sent message from this account
// (sent messages from the account will be added by the event loop).
if im.storeMailbox.LabelID() == pmapi.SentLabel {
sanitizedSender := pmapi.SanitizeEmail(m.Sender.Address)
// Check whether this message was sent by a bridge user.
user, err := im.user.backend.bridge.GetUser(sanitizedSender)
if err == nil && user.ID() == im.storeUser.UserID() {
logEntry := im.log.WithField("addr", sanitizedSender).WithField("extID", m.Header.Get("Message-Id"))
// If we find the message in the store already, we can skip importing it.
if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) {
logEntry.Info("Ignoring APPEND of duplicate to Sent folder")
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID})
}
// 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")
}
}
message.ParseFlags(m, flags)
if !date.IsZero() {
m.Time = date.Unix()
}
internalID := m.Header.Get("X-Pm-Internal-Id")
references := m.Header.Get("References")
referenceList := strings.Fields(references)
// In case there is a mail client which corrupts headers, try
// "References" too.
if internalID == "" && len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1]
match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference)
if len(match) == 2 {
internalID = match[1]
}
}
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
// Avoid appending a message which is already on the server. Apply the
// new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY).
if internalID != "" {
// Check to see if this belongs to a different address in split mode or another ProtonMail account.
msg, err := im.storeMailbox.GetMessage(internalID)
if err == nil && (im.user.user.IsCombinedAddressMode() || (im.storeAddress.AddressID() == msg.Message().AddressID)) {
IDs := []string{internalID}
// See the comment bellow.
if msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted(IDs); err != nil {
log.WithError(err).Error("Failed to undelete re-imported internal message")
}
}
err = im.storeMailbox.LabelMessages(IDs)
if err != nil {
return err
}
targetSeq := im.storeMailbox.GetUIDList(IDs)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
}
im.log.Info("Importing external message")
if err := im.importMessage(m, readers, kr); err != nil {
im.log.Error("Import failed: ", err)
return err
}
// IMAP clients can move message to local folder (setting \Deleted flag)
// and then move it back (IMAP client does not remember the message,
// so instead removing the flag it imports duplicate message).
// Regular IMAP server would keep the message twice and later EXPUNGE would
// not delete the message (EXPUNGE would delete the original message and
// the new duplicate one would stay). API detects duplicates; therefore
// we need to remove \Deleted flag if IMAP client re-imports.
msg, err := im.storeMailbox.GetMessage(m.ID)
if err == nil && msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted([]string{m.ID}); err != nil {
log.WithError(err).Error("Failed to undelete re-imported message")
}
}
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) { // nolint[funlen]
body, err := message.BuildEncrypted(m, readers, kr)
if err != nil {
return err
}
labels := []string{}
for _, l := range m.LabelIDs {
if l == pmapi.StarredLabel {
labels = append(labels, pmapi.StarredLabel)
}
}
return im.storeMailbox.ImportMessage(m, body, labels)
}
func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem, msgBuildCountHistogram *msgBuildCountHistogram) (msg *imap.Message, err error) { //nolint[funlen]
msglog := im.log.WithField("msgID", storeMessage.ID())
msglog.Trace("Getting message")
seqNum, err := storeMessage.SequenceNumber()
if err != nil {
return
}
m := storeMessage.Message()
msg = imap.NewMessage(seqNum, items)
for _, item := range items {
switch item {
case imap.FetchEnvelope:
// No need to check IsFullHeaderCached here. API header
// contain enough information to build the envelope.
msg.Envelope = message.GetEnvelope(m, storeMessage.GetHeader())
case imap.FetchBody, imap.FetchBodyStructure:
var structure *message.BodyStructure
structure, err = im.getBodyStructure(storeMessage)
if err != nil {
return
}
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
return
}
case imap.FetchFlags:
msg.Flags = message.GetFlags(m)
if storeMessage.IsMarkedDeleted() {
msg.Flags = append(msg.Flags, imap.DeletedFlag)
}
case imap.FetchInternalDate:
msg.InternalDate = time.Unix(m.Time, 0)
// Apple Mail crashes fetching messages with date older than 1970.
// There is no point having message older than RFC itself, it's not possible.
if msg.InternalDate.Before(rfc822Birthday) {
msg.InternalDate = rfc822Birthday
}
case imap.FetchRFC822Size:
// Size attribute on the server counts encrypted data. The value is cleared
// on our part and we need to compute "real" size of decrypted data.
if m.Size <= 0 {
msglog.Debug("Size unknown - downloading body")
// We are sure the size is not a problem right now. Clients
// might not first check sizes of all messages so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude getting size from the
// counting and see build count as real message build.
if _, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
return
}
}
msg.Size = uint32(m.Size)
case imap.FetchUid:
msg.Uid, err = storeMessage.UID()
if err != nil {
return nil, err
}
case imap.FetchAll, imap.FetchFast, imap.FetchFull, imap.FetchRFC822, imap.FetchRFC822Header, imap.FetchRFC822Text:
fallthrough // this is list of defined items by go-imap, but items can be also sections generated from requests
default:
if err = im.getLiteralForSection(item, msg, storeMessage, msgBuildCountHistogram); err != nil {
return
}
}
}
return msg, err
}
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) error {
section, err := imap.ParseBodySectionName(itemSection)
if err != nil {
log.WithError(err).Warn("Failed to parse body section name; part will be skipped")
return nil //nolint[nilerr] ignore error
}
var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section, msgBuildCountHistogram); err != nil {
return err
}
msg.Body[section] = literal
return nil
}
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *message.BodyStructure, err error) {
// Apple Mail requests body structure for all
// messages irregularly. We cache bodystructure in
// local database in order to not re-download all
// messages from server.
bs, err = storeMessage.GetBodyStructure()
if err != nil {
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
}
if bs == nil {
// We are sure the body structure is not a problem right now.
// Clients might do first fetch body structure so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude first body structure fetch
// from the counting and see build count as real message build.
if bs, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
return
}
}
return
}
//nolint[funlen] Jakub will fix in refactor
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) (
structure *message.BodyStructure,
bodyReader *bytes.Reader, err error,
) {
m := storeMessage.Message()
id := im.storeUser.UserID() + m.ID
cache.BuildLock(id)
if bodyReader, structure = cache.LoadMail(id); bodyReader.Len() == 0 || structure == nil {
var body []byte
structure, body, err = im.buildMessage(m)
m.Size = int64(len(body))
// Save size and body structure even for messages unable to decrypt
// so the size or body structure doesn't have to be computed every time.
if err := storeMessage.SetSize(m.Size); err != nil {
im.log.WithError(err).
WithField("newSize", m.Size).
WithField("msgID", m.ID).
Warn("Cannot update size while building")
}
if structure != nil && !isMessageInDraftFolder(m) {
if err := storeMessage.SetBodyStructure(structure); err != nil {
im.log.WithError(err).
WithField("msgID", m.ID).
Warn("Cannot update bodystructure while building")
}
}
if err == nil && structure != nil && len(body) > 0 {
header, errHead := structure.GetMailHeaderBytes(bytes.NewReader(body))
if errHead == nil {
if errHead := storeMessage.SetHeader(header); errHead != nil {
im.log.WithError(errHead).
WithField("msgID", m.ID).
Warn("Cannot update header after building")
}
} else {
im.log.WithError(errHead).
WithField("msgID", m.ID).
Warn("Cannot get header bytes after building")
}
if msgBuildCountHistogram != nil {
times, err := storeMessage.IncreaseBuildCount()
if err != nil {
im.log.WithError(err).
WithField("msgID", m.ID).
Warn("Cannot increase build count")
}
msgBuildCountHistogram.add(times)
}
// Drafts can change and we don't want to cache them.
if !isMessageInDraftFolder(m) {
cache.SaveMail(id, body, structure)
}
bodyReader = bytes.NewReader(body)
}
if _, ok := err.(*doNotCacheError); ok {
im.log.WithField("msgID", m.ID).Errorf("do not cache message: %v", err)
err = nil
bodyReader = bytes.NewReader(body)
}
}
cache.BuildUnlock(id)
return structure, bodyReader, err
}
func isMessageInDraftFolder(m *pmapi.Message) bool {
for _, labelID := range m.LabelIDs {
if labelID == pmapi.DraftLabel {
return true
}
}
return false
}
// This will download message (or read from cache) and pick up the section,
// extract data (header,body, both) and trim the output if needed.
func (im *imapMailbox) getMessageBodySection(
storeMessage storeMessageProvider,
section *imap.BodySectionName,
msgBuildCountHistogram *msgBuildCountHistogram,
) (imap.Literal, error) {
var header textproto.MIMEHeader
var response []byte
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body")
isMainHeaderRequested := len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier
if isMainHeaderRequested && storeMessage.IsFullHeaderCached() {
// In order to speed up (avoid download and decryptions) we
// cache the header. If a mail header was requested and DB
// contains full header (it means it was already built once)
// the DB header can be used without downloading and decrypting.
// Otherwise header is incomplete and clients would have issues
// e.g. AppleMail expects `text/plain` in HTML mails.
header = storeMessage.GetHeader()
} else {
// For all other cases it is necessary to download and decrypt the message
// and drop the header which was obtained from cache. The header will
// will be stored in DB once successfully built. Check `getBodyAndStructure`.
structure, bodyReader, err := im.getBodyAndStructure(storeMessage, msgBuildCountHistogram)
if err != nil {
return nil, err
}
switch {
case section.Specifier == imap.EntireSpecifier && len(section.Path) == 0:
// An empty section specification refers to the entire message, including the header.
response, err = structure.GetSection(bodyReader, section.Path)
case section.Specifier == imap.TextSpecifier || (section.Specifier == imap.EntireSpecifier && len(section.Path) != 0):
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header.
// Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
response, err = structure.GetSectionContent(bodyReader, section.Path)
case section.Specifier == imap.MIMESpecifier: // The MIME part specifier refers to the [MIME-IMB] header for this part.
fallthrough
case section.Specifier == imap.HeaderSpecifier:
header, err = structure.GetSectionHeader(section.Path)
default:
err = errors.New("Unknown specifier " + string(section.Specifier))
}
if err != nil {
return nil, err
}
}
if header != nil {
response = filteredHeaderAsBytes(header, section)
}
// Trim any output if requested.
return bytes.NewBuffer(section.ExtractPartial(response)), nil
}
// filteredHeaderAsBytes filters the header fields by section fields and it
// returns the filtered fields as bytes.
// Options are: all fields, only selected fields, all fields except selected.
func filteredHeaderAsBytes(header textproto.MIMEHeader, section *imap.BodySectionName) []byte {
// remove fields
if len(section.Fields) != 0 && section.NotFields {
for _, field := range section.Fields {
header.Del(field)
}
}
fields := make([]string, 0, len(header))
if len(section.Fields) == 0 || section.NotFields { // add all and sort
for f := range header {
fields = append(fields, f)
}
sort.Strings(fields)
} else { // add only requested (in requested order)
for _, f := range section.Fields {
fields = append(fields, textproto.CanonicalMIMEHeaderKey(f))
}
}
headerBuf := &bytes.Buffer{}
for _, canonical := range fields {
if values, ok := header[canonical]; !ok {
continue
} else {
for _, val := range values {
fmt.Fprintf(headerBuf, "%s: %s\r\n", canonical, val)
}
}
}
return headerBuf.Bytes()
}
// buildMessage from PM to IMAP.
func (im *imapMailbox) buildMessage(m *pmapi.Message) (*message.BodyStructure, []byte, error) {
body, err := im.builder.NewJobWithOptions(
context.Background(),
im.user.client(),
m.ID,
message.JobOptions{
IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead.
SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate.
AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id.
AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id.
AddMessageDate: true, // Whether to include message time as X-Pm-Date.
AddMessageIDReference: true, // Whether to include the MessageID in References.
},
).GetResult()
if err != nil {
return nil, nil, err
}
structure, err := message.NewBodyStructure(bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
return structure, body, nil
}

View File

@ -18,7 +18,6 @@
package imap package imap
import ( import (
"errors"
"fmt" "fmt"
"net/mail" "net/mail"
"strings" "strings"
@ -30,6 +29,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/parallel" "github.com/ProtonMail/proton-bridge/pkg/parallel"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -137,8 +137,14 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
return nil return nil
} }
func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flags []string) error { func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flags []string) error { //nolint[funlen]
for _, f := range flags { for _, f := range flags {
// Adding flag 'nojunk' is equivalent to removing flag 'junk'
if (operation == imap.AddFlags) && (f == "nojunk") {
operation = imap.RemoveFlags
f = "junk"
}
switch f { switch f {
case imap.SeenFlag: case imap.SeenFlag:
switch operation { //nolint[exhaustive] imap.SetFlags is processed by im.setFlags switch operation { //nolint[exhaustive] imap.SetFlags is processed by im.setFlags
@ -175,23 +181,37 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
} }
case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag: case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag:
// Not supported. // Not supported.
case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag: case strings.ToLower(message.AppleMailJunkFlag), strings.ToLower(message.ThunderbirdJunkFlag):
storeMailbox, err := im.storeAddress.GetMailbox("Spam") spamMailbox, err := im.storeAddress.GetMailbox("Spam")
if err != nil { if err != nil {
return err return err
} }
// Handle custom junk flags for Apple Mail and Thunderbird. // Handle custom junk flags for Apple Mail and Thunderbird.
switch operation { //nolint[exhaustive] imap.SetFlag is processed by im.setFlags switch operation { //nolint[exhaustive] imap.SetFlag is processed by im.setFlags
// No label removal is necessary because Spam and Inbox are both exclusive labels so the backend // No label removal is necessary because Spam and Inbox are both exclusive labels so the backend
// will automatically take care of label removal. // will automatically take care of label removal.
case imap.AddFlags: case imap.AddFlags:
if err := storeMailbox.LabelMessages(messageIDs); err != nil { if err := spamMailbox.LabelMessages(messageIDs); err != nil {
return err return err
} }
case imap.RemoveFlags: case imap.RemoveFlags:
if err := storeMailbox.UnlabelMessages(messageIDs); err != nil { // During spam flag removal only messages which
return err // are in Spam folder should be moved to Inbox.
// For other messages it is NOOP.
messagesInSpam := []string{}
for _, mID := range messageIDs {
if uid := spamMailbox.GetUIDList([]string{mID}); len(*uid) != 0 {
messagesInSpam = append(messagesInSpam, mID)
}
}
if len(messagesInSpam) != 0 {
inboxMailbox, err := im.storeAddress.GetMailbox("INBOX")
if err != nil {
return err
}
if err := inboxMailbox.LabelMessages(messagesInSpam); err != nil {
return err
}
} }
} }
} }
@ -230,6 +250,10 @@ func (im *imapMailbox) moveMessages(uid bool, seqSet *imap.SeqSet, targetLabel s
// 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()
// Moving from All Mail is not allowed.
if im.storeMailbox.LabelID() == pmapi.AllMailLabel {
return errors.New("move from All Mail is not allowed")
}
return im.labelMessages(uid, seqSet, targetLabel, true) return im.labelMessages(uid, seqSet, targetLabel, true)
} }
@ -359,9 +383,8 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
} }
} }
// In order to speed up search it is not needed to check // In order to speed up search it is not needed to check if IsFullHeaderCached.
// if IsFullHeaderCached. header := storeMessage.GetMIMEHeader()
header := storeMessage.GetHeader()
if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() { if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() {
t, err := mail.Header(header).Date() t, err := mail.Header(header).Date()
@ -422,7 +445,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
if isStringInList(m.LabelIDs, pmapi.StarredLabel) { if isStringInList(m.LabelIDs, pmapi.StarredLabel) {
messageFlagsMap[imap.FlaggedFlag] = true messageFlagsMap[imap.FlaggedFlag] = true
} }
if m.Unread == 0 { if !m.Unread {
messageFlagsMap[imap.SeenFlag] = true messageFlagsMap[imap.SeenFlag] = true
} }
if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) { if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) {
@ -526,18 +549,6 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima
return err return err
} }
// From RFC: UID range of 559:* always includes the UID of the last message
// in the mailbox, even if 559 is higher than any assigned UID value.
// See: https://tools.ietf.org/html/rfc3501#page-61
if isUID && seqSet.Dynamic() && len(apiIDs) == 0 {
l.Debug("Requesting empty UID dynamic fetch, adding latest message")
apiID, err := im.storeMailbox.GetLatestAPIID()
if err != nil {
return nil
}
apiIDs = []string{apiID}
}
input := make([]interface{}, len(apiIDs)) input := make([]interface{}, len(apiIDs))
for i, apiID := range apiIDs { for i, apiID := range apiIDs {
input[i] = apiID input[i] = apiID
@ -560,7 +571,7 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima
return nil, err return nil, err
} }
if storeMessage.Message().Unread == 1 { if storeMessage.Message().Unread {
for section := range msg.Body { for section := range msg.Body {
// Peek means get messages without marking them as read. // Peek means get messages without marking them as read.
// If client does not only ask for peek, we have to mark them as read. // If client does not only ask for peek, we have to mark them as read.

View File

@ -23,63 +23,76 @@ import (
"io" "io"
"net" "net"
"strings" "strings"
"sync/atomic"
"time" "time"
imapid "github.com/ProtonMail/go-imap-id" imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/imap/id" "github.com/ProtonMail/proton-bridge/internal/imap/id"
"github.com/ProtonMail/proton-bridge/internal/imap/idle"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
imapappendlimit "github.com/emersion/go-imap-appendlimit" imapappendlimit "github.com/emersion/go-imap-appendlimit"
imapidle "github.com/emersion/go-imap-idle"
imapmove "github.com/emersion/go-imap-move" imapmove "github.com/emersion/go-imap-move"
imapquota "github.com/emersion/go-imap-quota" imapquota "github.com/emersion/go-imap-quota"
imapunselect "github.com/emersion/go-imap-unselect" imapunselect "github.com/emersion/go-imap-unselect"
"github.com/emersion/go-imap/backend" "github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server" imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/sirupsen/logrus"
) )
type imapServer struct { // Server takes care of IMAP listening serving. It implements serverutil.Server.
panicHandler panicHandler type Server struct {
server *imapserver.Server panicHandler panicHandler
userAgent *useragent.UserAgent userAgent *useragent.UserAgent
eventListener listener.Listener debugClient bool
debugClient bool debugServer bool
debugServer bool port int
port int
isRunning atomic.Value server *imapserver.Server
controller serverutil.Controller
} }
// NewIMAPServer constructs a new IMAP server configured with the given options. // NewIMAPServer constructs a new IMAP server configured with the given options.
func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend backend.Backend, userAgent *useragent.UserAgent, eventListener listener.Listener) *imapServer { // nolint[golint] func NewIMAPServer(
s := imapserver.New(imapBackend) panicHandler panicHandler,
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) debugClient, debugServer bool,
s.TLSConfig = tls port int,
s.AllowInsecureAuth = true tls *tls.Config,
s.ErrorLog = newServerErrorLogger("server-imap") imapBackend backend.Backend,
s.AutoLogout = 30 * time.Minute userAgent *useragent.UserAgent,
eventListener listener.Listener,
if debugServer { ) *Server {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") server := &Server{
log.Warning("================================================") panicHandler: panicHandler,
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") userAgent: userAgent,
log.Warning("================================================") debugClient: debugClient,
debugServer: debugServer,
port: port,
} }
server.server = newGoIMAPServer(tls, imapBackend, server.Address(), userAgent)
server.controller = serverutil.NewController(server, eventListener)
return server
}
func newGoIMAPServer(tls *tls.Config, backend backend.Backend, address string, userAgent *useragent.UserAgent) *imapserver.Server {
server := imapserver.New(backend)
server.TLSConfig = tls
server.AllowInsecureAuth = true
server.ErrorLog = serverutil.NewServerErrorLogger(serverutil.IMAP)
server.AutoLogout = 30 * time.Minute
server.Addr = address
serverID := imapid.ID{ serverID := imapid.ID{
imapid.FieldName: "ProtonMail Bridge", imapid.FieldName: "ProtonMail Bridge",
imapid.FieldVendor: "Proton Technologies AG", imapid.FieldVendor: "Proton Technologies AG",
imapid.FieldSupportURL: "https://protonmail.com/support", imapid.FieldSupportURL: "https://protonmail.com/support",
} }
s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server { server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error { return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password) user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil { if err != nil {
@ -93,8 +106,8 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
}) })
}) })
s.Enable( server.Enable(
imapidle.NewExtension(), idle.NewExtension(),
imapmove.NewExtension(), imapmove.NewExtension(),
id.NewExtension(serverID, userAgent), id.NewExtension(serverID, userAgent),
imapquota.NewExtension(), imapquota.NewExtension(),
@ -103,195 +116,45 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
uidplus.NewExtension(), uidplus.NewExtension(),
) )
server := &imapServer{
panicHandler: panicHandler,
server: s,
userAgent: userAgent,
eventListener: eventListener,
debugClient: debugClient,
debugServer: debugServer,
port: port,
}
server.isRunning.Store(false)
return server return server
} }
// Starts the server. // ListenAndServe will run server and all monitors.
func (s *imapServer) ListenAndServe() { func (s *Server) ListenAndServe() { s.controller.ListenAndServe() }
go s.monitorDisconnectedUsers()
go s.monitorInternetConnection()
// When starting the Bridge, we don't want to retry to notify user // Close turns off server and monitors.
// quickly about the issue. Very probably retry will not help anyway. func (s *Server) Close() { s.controller.Close() }
s.listenAndServe(0)
// Implements serverutil.Server interface.
func (Server) Protocol() serverutil.Protocol { return serverutil.IMAP }
func (s *Server) UseSSL() bool { return false }
func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) }
func (s *Server) TLSConfig() *tls.Config { return s.server.TLSConfig }
func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() }
func (s *Server) DebugServer() bool { return s.debugServer }
func (s *Server) DebugClient() bool { return s.debugClient }
func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) {
s.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
if !s.userAgent.HasClient() {
s.userAgent.SetClient("UnknownClient", "0.0.1")
}
} }
func (s *imapServer) listenAndServe(retries int) { func (s *Server) DisconnectUser(address string) {
if s.isRunning.Load().(bool) { log.Info("Disconnecting all open IMAP connections for ", address)
return s.server.ForEachConn(func(conn imapserver.Conn) {
} connUser := conn.Context().User
s.isRunning.Store(true) if connUser != nil && strings.EqualFold(connUser.Username(), address) {
if err := conn.Close(); err != nil {
log.Info("IMAP server listening at ", s.server.Addr) log.WithError(err).Error("Failed to close the connection")
l, err := net.Listen("tcp", s.server.Addr) }
if err != nil {
s.isRunning.Store(false)
if retries > 0 {
log.WithError(err).WithField("retries", retries).Warn("IMAP listener failed")
time.Sleep(15 * time.Second)
s.listenAndServe(retries - 1)
return
} }
log.WithError(err).Error("IMAP listener failed")
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
return
}
err = s.server.Serve(&connListener{
Listener: l,
server: s,
userAgent: s.userAgent,
}) })
// Serve returns error every time, even after closing the server.
// User shouldn't be notified about error if server shouldn't be running,
// but it should in case it was not closed by `s.Close()`.
if err != nil && s.isRunning.Load().(bool) {
s.isRunning.Store(false)
log.WithError(err).Error("IMAP server failed")
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
return
}
defer s.server.Close() //nolint[errcheck]
log.Info("IMAP server stopped")
} }
// Stops the server. func (s *Server) Serve(listener net.Listener) error { return s.server.Serve(listener) }
func (s *imapServer) Close() { func (s *Server) StopServe() error { return s.server.Close() }
if !s.isRunning.Load().(bool) {
return
}
s.isRunning.Store(false)
log.Info("Closing IMAP server")
if err := s.server.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
}
}
func (s *imapServer) monitorInternetConnection() {
on := make(chan string)
s.eventListener.Add(events.InternetOnEvent, on)
off := make(chan string)
s.eventListener.Add(events.InternetOffEvent, off)
for {
var expectedIsPortFree bool
select {
case <-on:
go func() {
defer s.panicHandler.HandlePanic()
// We had issues on Mac that from time to time something
// blocked our port for a bit after we closed IMAP server
// due to connection issues.
// Restart always helped, so we do retry to not bother user.
s.listenAndServe(10)
}()
expectedIsPortFree = false
case <-off:
s.Close()
expectedIsPortFree = true
}
start := time.Now()
for {
if ports.IsPortFree(s.port) == expectedIsPortFree {
break
}
// Safety stop if something went wrong.
if time.Since(start) > 15*time.Second {
log.WithField("expectedIsPortFree", expectedIsPortFree).Warn("Server start/stop check timeouted")
break
}
time.Sleep(100 * time.Millisecond)
}
}
}
func (s *imapServer) monitorDisconnectedUsers() {
ch := make(chan string)
s.eventListener.Add(events.CloseConnectionEvent, ch)
for address := range ch {
address := address
log.Info("Disconnecting all open IMAP connections for ", address)
disconnectUser := func(conn imapserver.Conn) {
connUser := conn.Context().User
if connUser != nil && strings.EqualFold(connUser.Username(), address) {
if err := conn.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
}
}
}
s.server.ForEachConn(disconnectUser)
}
}
// connListener sets debug loggers on server containing fields with local
// and remote addresses right after new connection is accepted.
type connListener struct {
net.Listener
server *imapServer
userAgent *useragent.UserAgent
}
func (l *connListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err == nil && (l.server.debugServer || l.server.debugClient) {
debugLog := log
if addr := conn.LocalAddr(); addr != nil {
debugLog = debugLog.WithField("loc", addr.String())
}
if addr := conn.RemoteAddr(); addr != nil {
debugLog = debugLog.WithField("rem", addr.String())
}
var localDebug, remoteDebug io.Writer
if l.server.debugServer {
localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel)
}
if l.server.debugClient {
remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel)
}
l.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
}
if !l.userAgent.HasClient() {
l.userAgent.SetClient("UnknownClient", "0.0.1")
}
return conn, err
}
// serverErrorLogger implements go-imap/logger interface.
type serverErrorLogger struct {
tag string
}
func newServerErrorLogger(tag string) *serverErrorLogger {
return &serverErrorLogger{tag}
}
func (s *serverErrorLogger) Printf(format string, args ...interface{}) {
err := fmt.Sprintf(format, args...)
log.WithField("pkg", s.tag).Error(err)
}
func (s *serverErrorLogger) Println(args ...interface{}) {
err := fmt.Sprintln(args...)
log.WithField("pkg", s.tag).Error(err)
}

View File

@ -1,67 +0,0 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"fmt"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
imapserver "github.com/emersion/go-imap/server"
"github.com/stretchr/testify/require"
)
type testPanicHandler struct{}
func (ph *testPanicHandler) HandlePanic() {}
func TestIMAPServerTurnOffAndOnAgain(t *testing.T) {
panicHandler := &testPanicHandler{}
eventListener := listener.New()
port := ports.FindFreePortFrom(12345)
server := imapserver.New(nil)
server.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
s := &imapServer{
panicHandler: panicHandler,
server: server,
eventListener: eventListener,
userAgent: useragent.New(),
}
s.isRunning.Store(false)
go s.ListenAndServe()
time.Sleep(5 * time.Second)
require.False(t, ports.IsPortFree(port))
eventListener.Emit(events.InternetOffEvent, "")
time.Sleep(10 * time.Second)
require.True(t, ports.IsPortFree(port))
eventListener.Emit(events.InternetOnEvent, "")
time.Sleep(10 * time.Second)
require.False(t, ports.IsPortFree(port))
}

View File

@ -89,7 +89,7 @@ type storeMailboxProvider interface {
MarkMessagesUnstarred(apiID []string) error MarkMessagesUnstarred(apiID []string) error
MarkMessagesDeleted(apiID []string) error MarkMessagesDeleted(apiID []string) error
MarkMessagesUndeleted(apiID []string) error MarkMessagesUndeleted(apiID []string) error
ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error ImportMessage(enc []byte, seen bool, labelIDs []string, flags, time int64) (string, error)
RemoveDeleted(apiIDs []string) error RemoveDeleted(apiIDs []string) error
} }
@ -102,7 +102,8 @@ type storeMessageProvider interface {
SetSize(int64) error SetSize(int64) error
SetHeader([]byte) error SetHeader([]byte) error
GetHeader() textproto.MIMEHeader GetHeader() []byte
GetMIMEHeader() textproto.MIMEHeader
IsFullHeaderCached() bool IsFullHeaderCached() bool
SetBodyStructure(*pkgMsg.BodyStructure) error SetBodyStructure(*pkgMsg.BodyStructure) error
GetBodyStructure() (*pkgMsg.BodyStructure, error) GetBodyStructure() (*pkgMsg.BodyStructure, error)

View File

@ -188,10 +188,10 @@ func (iu *imapUpdates) MailboxStatus(address, mailboxName string, total, unread,
update.MailboxStatus.Messages = total update.MailboxStatus.Messages = total
update.MailboxStatus.Unseen = unread update.MailboxStatus.Unseen = unread
update.MailboxStatus.UnseenSeqNum = unreadSeqNum update.MailboxStatus.UnseenSeqNum = unreadSeqNum
iu.sendIMAPUpdate(update, false) iu.sendIMAPUpdate(update, true)
} }
func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, block bool) { func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, isBlocking bool) {
if iu.ch == nil { if iu.ch == nil {
log.Trace("IMAP IDLE unavailable") log.Trace("IMAP IDLE unavailable")
return return
@ -207,7 +207,7 @@ func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, block bool) {
} }
}() }()
if !block { if !isBlocking {
return return
} }

View File

@ -93,7 +93,7 @@ func newIMAPUser(
// This method should eventually no longer be necessary. Everything should go via store. // This method should eventually no longer be necessary. Everything should go via store.
func (iu *imapUser) client() pmapi.Client { func (iu *imapUser) client() pmapi.Client {
return iu.user.GetTemporaryPMAPIClient() return iu.user.GetClient()
} }
func (iu *imapUser) isSubscribed(labelID string) bool { func (iu *imapUser) isSubscribed(labelID string) bool {

View File

@ -20,7 +20,9 @@ package importexport
import ( import (
"bytes" "bytes"
"context"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/transfer" "github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -39,7 +41,8 @@ type ImportExport struct {
locations Locator locations Locator
cache Cacher cache Cacher
panicHandler users.PanicHandler panicHandler users.PanicHandler
clientManager users.ClientManager eventListener listener.Listener
clientManager pmapi.Manager
} }
func New( func New(
@ -47,7 +50,7 @@ func New(
cache Cacher, cache Cacher,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, clientManager pmapi.Manager,
credStorer users.CredentialsStorer, credStorer users.CredentialsStorer,
) *ImportExport { ) *ImportExport {
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, &storeFactory{}, false) u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, &storeFactory{}, false)
@ -58,63 +61,38 @@ func New(
locations: locations, locations: locations,
cache: cache, cache: cache,
panicHandler: panicHandler, panicHandler: panicHandler,
eventListener: eventListener,
clientManager: clientManager, clientManager: clientManager,
} }
} }
// ReportBug reports a new bug from the user. // ReportBug reports a new bug from the user.
func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error { func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
c := ie.clientManager.GetAnonymousClient() return ie.clientManager.ReportBug(context.Background(), pmapi.ReportBugReq{
defer c.Logout()
title := "[Import-Export] Bug"
report := pmapi.ReportReq{
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Browser: emailClient, Browser: emailClient,
Title: title, Title: "[Import-Export] Bug",
Description: description, Description: description,
Username: accountName, Username: accountName,
Email: address, Email: address,
} })
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
} }
// ReportFile submits import report file. // ReportFile submits import report file.
func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address string, logdata []byte) error { func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address string, logdata []byte) error {
c := ie.clientManager.GetAnonymousClient() report := pmapi.ReportBugReq{
defer c.Logout()
title := "[Import-Export] report file"
description := "An Import-Export report from the user swam down the river."
report := pmapi.ReportReq{
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Description: description, Description: "An Import-Export report from the user swam down the river.",
Title: title, Title: "[Import-Export] report file",
Username: accountName, Username: accountName,
Email: address, Email: address,
} }
report.AddAttachment("log", "report.log", bytes.NewReader(logdata)) report.AddAttachment("log", "report.log", bytes.NewReader(logdata))
if err := c.Report(report); err != nil { return ie.clientManager.ReportBug(context.Background(), report)
log.Error("Sending report failed: ", err)
return err
}
log.Info("Report successfully sent")
return nil
} }
// GetLocalImporter returns transferrer from local EML or MBOX structure to ProtonMail account. // GetLocalImporter returns transferrer from local EML or MBOX structure to ProtonMail account.
@ -187,5 +165,23 @@ func (ie *ImportExport) getPMAPIProvider(username, address string) (*transfer.PM
log.WithError(err).Info("Address does not exist, using all addresses") log.WithError(err).Info("Address does not exist, using all addresses")
} }
return transfer.NewPMAPIProvider(ie.clientManager, user.ID(), addressID) provider, err := transfer.NewPMAPIProvider(user.GetClient(), user.ID(), addressID)
if err != nil {
return nil, err
}
go func() {
internetOffCh := ie.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := ie.eventListener.ProvideChannel(events.InternetOnEvent)
for {
select {
case <-internetOffCh:
provider.SetConnectionDown()
case <-internetOnCh:
provider.SetConnectionUp()
}
}
}()
return provider, nil
} }

View File

@ -61,9 +61,17 @@ func NewReporter(appName, appVersion string, userAgent fmt.Stringer) *Reporter {
} }
func (r *Reporter) ReportException(i interface{}) error { func (r *Reporter) ReportException(i interface{}) error {
return r.ReportExceptionWithContext(i, make(map[string]interface{}))
}
func (r *Reporter) ReportMessage(msg string) error {
return r.ReportMessageWithContext(msg, make(map[string]interface{}))
}
func (r *Reporter) ReportExceptionWithContext(i interface{}, context map[string]interface{}) error {
err := fmt.Errorf("recover: %v", i) err := fmt.Errorf("recover: %v", i)
return r.scopedReport(func() { return r.scopedReport(context, func() {
if eventID := sentry.CaptureException(err); eventID != nil { if eventID := sentry.CaptureException(err); eventID != nil {
logrus.WithError(err). logrus.WithError(err).
WithField("reportID", *eventID). WithField("reportID", *eventID).
@ -72,8 +80,8 @@ func (r *Reporter) ReportException(i interface{}) error {
}) })
} }
func (r *Reporter) ReportMessage(msg string) error { func (r *Reporter) ReportMessageWithContext(msg string, context map[string]interface{}) error {
return r.scopedReport(func() { return r.scopedReport(context, func() {
if eventID := sentry.CaptureMessage(msg); eventID != nil { if eventID := sentry.CaptureMessage(msg); eventID != nil {
logrus.WithField("message", msg). logrus.WithField("message", msg).
WithField("reportID", *eventID). WithField("reportID", *eventID).
@ -83,7 +91,7 @@ func (r *Reporter) ReportMessage(msg string) error {
} }
// Report reports a sentry crash with stacktrace from all goroutines. // Report reports a sentry crash with stacktrace from all goroutines.
func (r *Reporter) scopedReport(doReport func()) error { func (r *Reporter) scopedReport(context map[string]interface{}, doReport func()) error {
SkipDuringUnwind() SkipDuringUnwind()
if os.Getenv("PROTONMAIL_ENV") == "dev" { if os.Getenv("PROTONMAIL_ENV") == "dev" {
@ -101,6 +109,7 @@ func (r *Reporter) scopedReport(doReport func()) error {
sentry.WithScope(func(scope *sentry.Scope) { sentry.WithScope(func(scope *sentry.Scope) {
SkipDuringUnwind() SkipDuringUnwind()
scope.SetTags(tags) scope.SetTags(tags)
scope.SetContexts(context)
doReport() doReport()
}) })

View File

@ -0,0 +1,117 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package serverutil
import (
"crypto/tls"
"fmt"
"net"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
)
// Controller will make sure that server is listening and serving and if needed
// users are disconnected.
type Controller interface {
ListenAndServe()
Close()
}
// NewController return simple server controller.
func NewController(s Server, l listener.Listener) Controller {
log := logrus.WithField("pkg", "serverutil").WithField("protocol", s.Protocol())
c := &controller{
server: s,
signals: l,
log: log,
closeDisconnectUsers: make(chan void),
}
if s.DebugServer() {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
}
return c
}
type void struct{}
type controller struct {
server Server
signals listener.Listener
log *logrus.Entry
closeDisconnectUsers chan void
}
func (c *controller) Close() {
c.closeDisconnectUsers <- void{}
if err := c.server.StopServe(); err != nil {
c.log.WithError(err).Error("Issue when closing server")
}
}
// ListenAndServe starts the server and keeps it on based on internet
// availability. It also monitors and disconnect users if requested.
func (c *controller) ListenAndServe() {
go monitorDisconnectedUsers(c.server, c.signals, c.closeDisconnectUsers)
defer c.server.HandlePanic()
l := c.log.WithField("useSSL", c.server.UseSSL()).
WithField("address", c.server.Address())
var listener net.Listener
var err error
if c.server.UseSSL() {
listener, err = tls.Listen("tcp", c.server.Address(), c.server.TLSConfig())
} else {
listener, err = net.Listen("tcp", c.server.Address())
}
if err != nil {
l.WithError(err).Error("Cannot start listner.")
c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error())
return
}
// When starting the Bridge, we don't want to retry to notify user
// quickly about the issue. Very probably retry will not help anyway.
l.Info("Starting server")
err = c.server.Serve(&connListener{listener, c.server})
l.WithError(err).Debug("GoSMTP not serving")
}
func monitorDisconnectedUsers(s Server, l listener.Listener, done <-chan void) {
ch := make(chan string)
l.Add(events.CloseConnectionEvent, ch)
for {
select {
case <-done:
return
case address := <-ch:
s.DisconnectUser(address)
}
}
}

View File

@ -15,29 +15,25 @@
// 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 pmapi package serverutil
import ( import (
"net/url" "github.com/sirupsen/logrus"
) )
// SendSimpleMetric makes a simple GET request to send a simple metrics report. // ServerErrorLogger implements go-imap/logger interface.
func (c *client) SendSimpleMetric(category, action, label string) (err error) { type ServerErrorLogger struct {
v := url.Values{} l *logrus.Entry
v.Set("Category", category) }
v.Set("Action", action)
v.Set("Label", label) func NewServerErrorLogger(protocol Protocol) *ServerErrorLogger {
return &ServerErrorLogger{l: logrus.WithField("protocol", protocol)}
req, err := c.NewRequest("GET", "/metrics?"+v.Encode(), nil) }
if err != nil {
return func (s *ServerErrorLogger) Printf(format string, args ...interface{}) {
} s.l.Errorf(format, args...)
}
var res Res
if err = c.DoJSON(req, &res); err != nil { func (s *ServerErrorLogger) Println(args ...interface{}) {
return s.l.Errorln(args...)
}
err = res.Err()
return
} }

View File

@ -0,0 +1,59 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package serverutil
import (
"io"
"net"
"github.com/sirupsen/logrus"
)
// connListener sets debug loggers on server containing fields with local
// and remote addresses right after new connection is accepted.
type connListener struct {
net.Listener
server Server
}
func (l *connListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err == nil && (l.server.DebugServer() || l.server.DebugClient()) {
debugLog := logrus.WithField("pkg", l.server.Protocol())
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 l.server.DebugServer() {
localDebug = debugLog.WithField("comm", "server").WriterLevel(logrus.DebugLevel)
}
if l.server.DebugClient() {
remoteDebug = debugLog.WithField("comm", "client").WriterLevel(logrus.DebugLevel)
}
l.server.SetLoggers(localDebug, remoteDebug)
}
return conn, err
}

View File

@ -15,9 +15,12 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package users package serverutil
// IsAuthorized returns whether the user has received an Auth from the API yet. type Protocol string
func (u *User) IsAuthorized() bool {
return u.isAuthorized const (
} HTTP = Protocol("HTTP")
IMAP = Protocol("IMAP")
SMTP = Protocol("SMTP")
)

View File

@ -0,0 +1,41 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package serverutil
import (
"crypto/tls"
"io"
"net"
)
// Server can handle disconnected users.
type Server interface {
Protocol() Protocol
UseSSL() bool
Address() string
TLSConfig() *tls.Config
DebugServer() bool
DebugClient() bool
SetLoggers(localDebug, remoteDebug io.Writer)
HandlePanic()
DisconnectUser(string)
Serve(net.Listener) error
StopServe() error
}

View File

@ -0,0 +1,153 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package test
import (
"net/http"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/stretchr/testify/require"
)
func setup(t *testing.T) (*require.Assertions, *testServer, listener.Listener, serverutil.Controller) {
r := require.New(t)
s := newTestServer()
l := listener.New()
c := serverutil.NewController(s, l)
return r, s, l, c
}
func TestControllerListernServeClose(t *testing.T) {
r, s, l, c := setup(t)
errorCh := l.ProvideChannel(events.ErrorEvent)
r.True(s.portIsFree())
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.Nil(s.localDebug)
r.Nil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
select {
case msg := <-errorCh:
r.Fail("Expected no error but have %q", msg)
case <-time.Tick(100 * time.Millisecond):
break
}
}
func TestControllerFailOnBusyPort(t *testing.T) {
r, s, l, c := setup(t)
ocupator := http.Server{Addr: s.Address()}
defer ocupator.Close() //nolint[errcheck]
go ocupator.ListenAndServe() //nolint[errcheck]
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
errorCh := l.ProvideChannel(events.ErrorEvent)
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
select {
case <-errorCh:
break
case <-time.Tick(time.Second):
r.Fail("Expected error but have none.")
}
}
func TestControllerCallDisconnectUser(t *testing.T) {
r, s, l, c := setup(t)
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
l.Emit(events.CloseConnectionEvent, "")
r.Eventually(func() bool { return s.calledDisconnected == 1 }, time.Second, 50*time.Millisecond)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
l.Emit(events.CloseConnectionEvent, "")
r.Equal(1, s.calledDisconnected)
}
func TestDebugClient(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = false
s.debugClient = true
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.Nil(s.localDebug)
r.NotNil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}
func TestDebugServer(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = true
s.debugClient = false
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.NotNil(s.localDebug)
r.Nil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}
func TestDebugBoth(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = true
s.debugClient = true
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.NotNil(s.localDebug)
r.NotNil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}

View File

@ -0,0 +1,88 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package test
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/ports"
)
func newTestServer() *testServer {
return &testServer{port: 11188}
}
type testServer struct {
http http.Server
useSSL,
debugServer,
debugClient bool
calledDisconnected int
port int
tls *tls.Config
localDebug, remoteDebug io.Writer
}
func (*testServer) Protocol() serverutil.Protocol { return serverutil.HTTP }
func (s *testServer) UseSSL() bool { return s.useSSL }
func (s *testServer) Address() string { return fmt.Sprintf("127.0.0.1:%d", s.port) }
func (s *testServer) TLSConfig() *tls.Config { return s.tls }
func (s *testServer) HandlePanic() {}
func (s *testServer) DebugServer() bool { return s.debugServer }
func (s *testServer) DebugClient() bool { return s.debugClient }
func (s *testServer) SetLoggers(localDebug, remoteDebug io.Writer) {
s.localDebug = localDebug
s.remoteDebug = remoteDebug
}
func (s *testServer) DisconnectUser(string) {
s.calledDisconnected++
}
func (s *testServer) Serve(l net.Listener) error {
return s.http.Serve(l)
}
func (s *testServer) StopServe() error { return s.http.Close() }
func (s *testServer) portIsFree() bool {
return ports.IsPortFree(s.port)
}
func (s *testServer) portIsOccupied() bool {
return !ports.IsPortFree(s.port)
}
func (s *testServer) ping() error {
client := &http.Client{}
resp, err := client.Get("http://" + s.Address() + "/ping")
if err != nil {
return err
}
return resp.Body.Close()
}

View File

@ -31,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() pmapi.Client GetClient() pmapi.Client
GetStore() storeUserProvider GetStore() storeUserProvider
} }

View File

@ -48,7 +48,7 @@ func dumpMessageData(b []byte, subject string) {
} }
if err := ioutil.WriteFile( if err := ioutil.WriteFile(
filepath.Join(path, fmt.Sprintf("%v-%v.eml", subject, time.Now().Format(time.RFC3339Nano))), filepath.Join(path, fmt.Sprintf("%v-%v.eml", subject, time.Now().Unix())),
b, b,
0600, 0600,
); err != nil { ); err != nil {

View File

@ -18,6 +18,7 @@
package smtp package smtp
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"strings" "strings"
@ -28,7 +29,7 @@ import (
) )
type messageGetter interface { type messageGetter interface {
GetMessage(string) (*pmapi.Message, error) GetMessage(context.Context, string) (*pmapi.Message, error)
} }
type sendRecorderValue struct { type sendRecorderValue struct {
@ -126,7 +127,7 @@ func (q *sendRecorder) isSendingOrSent(client messageGetter, hash string) (isSen
return true, false return true, false
} }
message, err := client.GetMessage(value.messageID) message, err := client.GetMessage(context.TODO(), value.messageID)
// Message could be deleted or there could be an internet issue or whatever, // Message could be deleted or there could be an internet issue or whatever,
// so let's assume the message was not sent. // so let's assume the message was not sent.
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@
package smtp package smtp
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/mail" "net/mail"
@ -33,7 +34,7 @@ type testSendRecorderGetMessageMock struct {
err error err error
} }
func (m *testSendRecorderGetMessageMock) GetMessage(messageID string) (*pmapi.Message, error) { func (m *testSendRecorderGetMessageMock) GetMessage(_ context.Context, messageID string) (*pmapi.Message, error) {
return m.message, m.err return m.message, m.err
} }

View File

@ -20,44 +20,61 @@ package smtp
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io"
"net"
"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/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
goSMTP "github.com/emersion/go-smtp" goSMTP "github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
) )
type smtpServer struct { // Server is Bridge SMTP server implementation.
server *goSMTP.Server type Server struct {
eventListener listener.Listener panicHandler panicHandler
useSSL bool backend goSMTP.Backend
debug bool
useSSL bool
port int
tls *tls.Config
server *goSMTP.Server
controller serverutil.Controller
} }
// NewSMTPServer returns an SMTP server configured with the given options. // NewSMTPServer returns an SMTP server configured with the given options.
func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBackend goSMTP.Backend, eventListener listener.Listener) *smtpServer { //nolint[golint] func NewSMTPServer(
s := goSMTP.NewServer(smtpBackend) panicHandler panicHandler,
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) debug bool, port int, useSSL bool,
s.TLSConfig = tls tls *tls.Config,
s.Domain = bridge.Host smtpBackend goSMTP.Backend,
s.AllowInsecureAuth = true eventListener listener.Listener,
s.MaxLineLength = 2 << 16 ) *Server {
server := &Server{
if debug { panicHandler: panicHandler,
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") backend: smtpBackend,
log.Warning("================================================") debug: debug,
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") useSSL: useSSL,
log.Warning("================================================") port: port,
tls: tls,
} }
if debug { server.server = newGoSMTPServer(server)
s.Debug = logrus. server.controller = serverutil.NewController(server, eventListener)
WithField("pkg", "smtp/server"). return server
WriterLevel(logrus.DebugLevel) }
}
s.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server { func newGoSMTPServer(s *Server) *goSMTP.Server {
newSMTP := goSMTP.NewServer(s.backend)
newSMTP.Addr = s.Address()
newSMTP.TLSConfig = s.tls
newSMTP.Domain = bridge.Host
newSMTP.ErrorLog = serverutil.NewServerErrorLogger(serverutil.SMTP)
newSMTP.AllowInsecureAuth = true
newSMTP.MaxLineLength = 1 << 16
newSMTP.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error { return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password) user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil { if err != nil {
@ -68,57 +85,39 @@ func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBacke
return nil return nil
}) })
}) })
return newSMTP
return &smtpServer{
server: s,
eventListener: eventListener,
useSSL: useSSL,
}
} }
// Starts the server. // ListenAndServe will run server and all monitors.
func (s *smtpServer) ListenAndServe() { func (s *Server) ListenAndServe() { s.controller.ListenAndServe() }
go s.monitorDisconnectedUsers()
l := log.WithField("useSSL", s.useSSL).WithField("address", s.server.Addr)
l.Info("SMTP server is starting") // Close turns off server and monitors.
var err error func (s *Server) Close() { s.controller.Close() }
if s.useSSL {
err = s.server.ListenAndServeTLS()
} else {
err = s.server.ListenAndServe()
}
if err != nil {
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
l.Error("SMTP failed: ", err)
return
}
defer s.server.Close() //nolint[errcheck]
l.Info("SMTP server stopped") // Implements servertutil.Server interface.
}
// Stops the server. func (Server) Protocol() serverutil.Protocol { return serverutil.SMTP }
func (s *smtpServer) Close() { func (s *Server) UseSSL() bool { return s.useSSL }
if err := s.server.Close(); err != nil { func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) }
log.WithError(err).Error("Failed to close the connection") func (s *Server) TLSConfig() *tls.Config { return s.tls }
} func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() }
}
func (s *smtpServer) monitorDisconnectedUsers() { func (s *Server) DebugServer() bool { return s.debug }
ch := make(chan string) func (s *Server) DebugClient() bool { return s.debug }
s.eventListener.Add(events.CloseConnectionEvent, ch)
for address := range ch { func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) { s.server.Debug = localDebug }
log.Info("Disconnecting all open SMTP connections for ", address)
disconnectUser := func(conn *goSMTP.Conn) { func (s *Server) DisconnectUser(address string) {
connUser := conn.Session() log.Info("Disconnecting all open SMTP connections for ", address)
if connUser != nil { s.server.ForEachConn(func(conn *goSMTP.Conn) {
if err := conn.Close(); err != nil { connUser := conn.Session()
log.WithError(err).Error("Failed to close the connection") if connUser != nil {
} if err := conn.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
} }
} }
s.server.ForEachConn(disconnectUser) })
}
} }
func (s *Server) Serve(l net.Listener) error { return s.server.Serve(l) }
func (s *Server) StopServe() error { return s.server.Close() }

View File

@ -21,10 +21,10 @@ package smtp
import ( import (
"bytes" "bytes"
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"mime" "mime"
"net/mail" "net/mail"
"strings" "strings"
@ -81,7 +81,7 @@ func newSMTPUser(
// This method should eventually no longer be necessary. Everything should go via store. // This method should eventually no longer be necessary. Everything should go via store.
func (su *smtpUser) client() pmapi.Client { func (su *smtpUser) client() pmapi.Client {
return su.user.GetTemporaryPMAPIClient() return su.user.GetClient()
} }
// 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.
@ -123,7 +123,7 @@ func (su *smtpUser) getSendPreferences(
} }
func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata, err error) { func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata, err error) {
emails, err := su.client().GetContactEmailByEmail(recipient, 0, 1000) emails, err := su.client().GetContactEmailByEmail(context.TODO(), recipient, 0, 1000)
if err != nil { if err != nil {
return return
} }
@ -135,7 +135,7 @@ func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata
} }
var contact pmapi.Contact var contact pmapi.Contact
if contact, err = su.client().GetContactByID(email.ContactID); err != nil { if contact, err = su.client().GetContactByID(context.TODO(), email.ContactID); err != nil {
return return
} }
@ -151,7 +151,7 @@ func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata
} }
func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey, isInternal bool, err error) { func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey, isInternal bool, err error) {
return su.client().GetPublicKeysForEmail(recipient) return su.client().GetPublicKeysForEmail(context.TODO(), recipient)
} }
// Discard currently processed message. // Discard currently processed message.
@ -219,7 +219,7 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
messageReader = io.TeeReader(messageReader, b) messageReader = io.TeeReader(messageReader, b)
mailSettings, err := su.client().GetMailSettings() mailSettings, err := su.client().GetMailSettings(context.TODO())
if err != nil { if err != nil {
return err return err
} }
@ -325,12 +325,6 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
return nil return nil
} }
if ok, err := su.isTotalSizeOkay(message, attReaders); err != nil {
return err
} else if !ok {
return errors.New("message is too large")
}
su.backend.sendRecorder.addMessage(sendRecorderMessageHash) su.backend.sendRecorder.addMessage(sendRecorderMessageHash)
message, atts, err := su.storeUser.CreateDraft(kr, message, attReaders, attachedPublicKey, attachedPublicKeyName, parentID) message, atts, err := su.storeUser.CreateDraft(kr, message, attReaders, attachedPublicKey, attachedPublicKeyName, parentID)
if err != nil { if err != nil {
@ -346,7 +340,7 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
// 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(context.TODO(), []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")
} }
} }
@ -368,7 +362,8 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
req := pmapi.NewSendMessageReq(kr, mimeBody, plainBody, richBody, attkeys) req := pmapi.NewSendMessageReq(kr, mimeBody, plainBody, richBody, attkeys)
containsUnencryptedRecipients := false containsUnencryptedRecipients := false
for _, email := range to { for _, recipient := range message.Recipients() {
email := recipient.Address
if !looksLikeEmail(email) { if !looksLikeEmail(email) {
return errors.New(`"` + email + `" is not a valid recipient.`) return errors.New(`"` + email + `" is not a valid recipient.`)
} }
@ -400,7 +395,7 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
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) {
if err := su.client().DeleteMessages([]string{message.ID}); err != nil { if err := su.client().DeleteMessages(context.TODO(), []string{message.ID}); err != nil {
log.WithError(err).Warn("Failed to delete canceled messages") log.WithError(err).Warn("Failed to delete canceled messages")
} }
return errors.New("sending was canceled by user") return errors.New("sending was canceled by user")
@ -429,7 +424,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(context.TODO(), filter)
for _, m := range metadata { for _, m := range metadata {
if m.IsDraft() { if m.IsDraft() {
draftID = m.ID draftID = m.ID
@ -449,7 +444,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(context.TODO(), 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 {
@ -541,24 +536,3 @@ func (su *smtpUser) Logout() error {
log.Debug("SMTP client logged out user ", su.addressID) log.Debug("SMTP client logged out user ", su.addressID)
return nil return nil
} }
func (su *smtpUser) isTotalSizeOkay(message *pmapi.Message, attReaders []io.Reader) (bool, error) {
maxUpload, err := su.storeUser.GetMaxUpload()
if err != nil {
return false, err
}
var attSize int64
for i := range attReaders {
b, err := ioutil.ReadAll(attReaders[i])
if err != nil {
return false, err
}
attSize += int64(len(b))
attReaders[i] = bytes.NewBuffer(b)
}
return message.Size+attSize <= maxUpload, nil
}

View File

@ -90,7 +90,7 @@ func getLabelPrefix(l *pmapi.Label) string {
switch { switch {
case pmapi.IsSystemLabel(l.ID): case pmapi.IsSystemLabel(l.ID):
return "" return ""
case l.Exclusive == 1: case bool(l.Exclusive):
return UserFoldersPrefix return UserFoldersPrefix
default: default:
return UserLabelsPrefix return UserLabelsPrefix

View File

@ -37,8 +37,8 @@ func TestNotifyChangeCreateOrUpdateMessage(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.store.SetChangeNotifier(m.changeNotifier) m.store.SetChangeNotifier(m.changeNotifier)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel})
} }
func TestNotifyChangeCreateOrUpdateMessages(t *testing.T) { func TestNotifyChangeCreateOrUpdateMessages(t *testing.T) {
@ -52,8 +52,8 @@ func TestNotifyChangeCreateOrUpdateMessages(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.store.SetChangeNotifier(m.changeNotifier) m.store.SetChangeNotifier(m.changeNotifier)
msg1 := getTestMessage("msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) msg1 := getTestMessage("msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
msg2 := getTestMessage("msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) msg2 := getTestMessage("msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel})
require.Nil(t, m.store.createOrUpdateMessagesEvent([]*pmapi.Message{msg1, msg2})) require.Nil(t, m.store.createOrUpdateMessagesEvent([]*pmapi.Message{msg1, msg2}))
} }
@ -63,8 +63,8 @@ func TestNotifyChangeDeleteMessage(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel})
m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(2)) m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(2))
m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(1)) m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(1))

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"math/rand" "math/rand"
"time" "time"
@ -80,7 +81,7 @@ func (loop *eventLoop) client() pmapi.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.client().GetEvent("") event, err := loop.client().GetEvent(context.Background(), "")
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
@ -99,6 +100,11 @@ func (loop *eventLoop) setFirstEventID() (err error) {
// pollNow starts polling events right away and waits till the events are // pollNow starts polling events right away and waits till the events are
// processed so we are sure updates are propagated to the database. // processed so we are sure updates are propagated to the database.
func (loop *eventLoop) pollNow() { func (loop *eventLoop) pollNow() {
// When event loop is not running, it would cause infinite wait.
if !loop.isRunning {
return
}
eventProcessedCh := make(chan struct{}) eventProcessedCh := make(chan struct{})
loop.pollCh <- eventProcessedCh loop.pollCh <- eventProcessedCh
<-eventProcessedCh <-eventProcessedCh
@ -216,7 +222,7 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
// We only want to consider invalid tokens as real errors because all other errors might fix themselves eventually // We only want to consider invalid tokens as real errors because all other errors might fix themselves eventually
// (e.g. no internet, ulimit reached etc.) // (e.g. no internet, ulimit reached etc.)
defer func() { defer func() {
if errors.Cause(err) == pmapi.ErrAPINotReachable { if errors.Cause(err) == pmapi.ErrNoConnection {
l.Warn("Internet unavailable") l.Warn("Internet unavailable")
err = nil err = nil
} }
@ -232,17 +238,21 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
err = nil err = nil
} }
_, errUnauthorized := errors.Cause(err).(*pmapi.ErrUnauthorized)
if err == nil { if err == nil {
loop.errCounter = 0 loop.errCounter = 0
} }
// All errors except Invalid Token (which is not possible to recover from) are ignored.
if err != nil && !errUnauthorized && errors.Cause(err) != pmapi.ErrInvalidToken { // All errors except ErrUnauthorized (which is not possible to recover from) are ignored.
if err != nil && errors.Cause(err) != pmapi.ErrUnauthorized {
l.WithError(err).WithField("errors", loop.errCounter).Error("Error skipped") l.WithError(err).WithField("errors", loop.errCounter).Error("Error skipped")
loop.errCounter++ loop.errCounter++
if loop.errCounter == errMaxSentry { if loop.errCounter == errMaxSentry {
if sentryErr := loop.store.sentryReporter.ReportMessage("Warning: event loop issues: " + err.Error() + ", " + loop.currentEventID); sentryErr != nil { context := map[string]interface{}{
"EventLoop": map[string]interface{}{
"EventID": loop.currentEventID,
},
}
if sentryErr := loop.store.sentryReporter.ReportMessageWithContext("Warning: event loop issues: "+err.Error(), context); sentryErr != nil {
l.WithError(sentryErr).Error("Failed to report error to sentry") l.WithError(sentryErr).Error("Failed to report error to sentry")
} }
} }
@ -259,7 +269,7 @@ 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.client().GetEvent(loop.currentEventID); err != nil { if event, err = loop.client().GetEvent(context.Background(), loop.currentEventID); err != nil {
return false, errors.Wrap(err, "failed to get event") return false, errors.Wrap(err, "failed to get event")
} }
@ -286,7 +296,7 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
} }
} }
return event.More == 1, err return bool(event.More), err
} }
func (loop *eventLoop) processEvent(event *pmapi.Event) (err error) { func (loop *eventLoop) processEvent(event *pmapi.Event) (err error) {
@ -297,7 +307,12 @@ func (loop *eventLoop) processEvent(event *pmapi.Event) (err error) {
eventLog.Info("Processing refresh event") eventLog.Info("Processing refresh event")
loop.store.triggerSync() loop.store.triggerSync()
if sentryErr := loop.store.sentryReporter.ReportMessage("Warning: refresh occurred, " + loop.currentEventID); sentryErr != nil { context := map[string]interface{}{
"EventLoop": map[string]interface{}{
"EventID": loop.currentEventID,
},
}
if sentryErr := loop.store.sentryReporter.ReportMessageWithContext("Warning: refresh occurred", context); sentryErr != nil {
loop.log.WithError(sentryErr).Error("Failed to report refresh to sentry") loop.log.WithError(sentryErr).Error("Failed to report refresh to sentry")
} }
@ -345,7 +360,7 @@ func (loop *eventLoop) processAddresses(log *logrus.Entry, addressEvents []*pmap
// Get old addresses for comparisons before updating user. // Get old addresses for comparisons before updating user.
oldList := loop.client().Addresses() oldList := loop.client().Addresses()
if err = loop.user.UpdateUser(); err != nil { if err = loop.user.UpdateUser(context.Background()); err != nil {
if logoutErr := loop.user.Logout(); logoutErr != nil { if logoutErr := loop.user.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Failed to logout user after failed update") log.WithError(logoutErr).Error("Failed to logout user after failed update")
} }
@ -456,8 +471,8 @@ 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.client().GetMessage(message.ID); err != nil { if msg, err = loop.client().GetMessage(context.Background(), 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
continue continue

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"net/mail" "net/mail"
"testing" "testing"
"time" "time"
@ -39,17 +40,17 @@ 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.client.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{ m.client.EXPECT().GetEvent(gomock.Any(), "latestEventID").Return(&pmapi.Event{
EventID: "event50", EventID: "event50",
More: 1, More: true,
}, nil), }, nil),
m.client.EXPECT().GetEvent("event50").Return(&pmapi.Event{ m.client.EXPECT().GetEvent(gomock.Any(), "event50").Return(&pmapi.Event{
EventID: "event70", EventID: "event70",
More: 0, More: false,
}, nil), }, nil),
m.client.EXPECT().GetEvent("event70").Return(&pmapi.Event{ m.client.EXPECT().GetEvent(gomock.Any(), "event70").Return(&pmapi.Event{
EventID: "event71", EventID: "event71",
More: 0, More: false,
}, nil), }, nil),
) )
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
@ -165,7 +166,7 @@ func TestEventLoopDeletionPaused(t *testing.T) {
func testEvent(t *testing.T, m *mocksForStore, event *pmapi.Event) { func testEvent(t *testing.T, m *mocksForStore, event *pmapi.Event) {
eventReceived := make(chan struct{}) eventReceived := make(chan struct{})
m.client.EXPECT().GetEvent("latestEventID").DoAndReturn(func(eventID string) (*pmapi.Event, error) { m.client.EXPECT().GetEvent(gomock.Any(), "latestEventID").DoAndReturn(func(_ context.Context, eventID string) (*pmapi.Event, error) {
defer close(eventReceived) defer close(eventReceived)
return event, nil return event, nil
}) })
@ -187,7 +188,7 @@ func TestEventLoopUpdateMessage(t *testing.T) {
msg := &pmapi.Message{ msg := &pmapi.Message{
ID: "msg1", ID: "msg1",
Subject: "old", Subject: "old",
Unread: 0, Unread: false,
Flags: 10, Flags: 10,
Sender: address1, Sender: address1,
ToList: []*mail.Address{address2}, ToList: []*mail.Address{address2},
@ -199,7 +200,7 @@ func TestEventLoopUpdateMessage(t *testing.T) {
newMsg := &pmapi.Message{ newMsg := &pmapi.Message{
ID: "msg1", ID: "msg1",
Subject: "new", Subject: "new",
Unread: 1, Unread: true,
Flags: 11, Flags: 11,
Sender: address2, Sender: address2,
ToList: []*mail.Address{address1}, ToList: []*mail.Address{address1},

View File

@ -129,17 +129,10 @@ func (mc *mailboxCounts) getPMLabel() *pmapi.Label {
Color: mc.Color, Color: mc.Color,
Order: mc.Order, Order: mc.Order,
Type: pmapi.LabelTypeMailbox, Type: pmapi.LabelTypeMailbox,
Exclusive: mc.isExclusive(), Exclusive: pmapi.Boolean(mc.IsFolder),
} }
} }
func (mc *mailboxCounts) isExclusive() int {
if mc.IsFolder {
return 1
}
return 0
}
// createOrUpdateMailboxCountsBuckets will not change the on-API-counts. // createOrUpdateMailboxCountsBuckets will not change the on-API-counts.
func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) error { func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) error {
// Don't forget about system folders. // Don't forget about system folders.
@ -162,7 +155,7 @@ func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) er
mailbox.LabelName = label.Path mailbox.LabelName = label.Path
mailbox.Color = label.Color mailbox.Color = label.Color
mailbox.Order = label.Order mailbox.Order = label.Order
mailbox.IsFolder = label.Exclusive == 1 mailbox.IsFolder = bool(label.Exclusive)
// Write. // Write.
if err = mailbox.txWriteToBucket(countsBkt); err != nil { if err = mailbox.txWriteToBucket(countsBkt); err != nil {

View File

@ -75,7 +75,7 @@ func TestMailboxNames(t *testing.T) {
newLabel(100, "labelID1", "Label1"), newLabel(100, "labelID1", "Label1"),
newLabel(1000, "folderID1", "Folder1"), newLabel(1000, "folderID1", "Folder1"),
} }
foldersAndLabels[1].Exclusive = 1 foldersAndLabels[1].Exclusive = true
for _, counts := range getSystemFolders() { for _, counts := range getSystemFolders() {
foldersAndLabels = append(foldersAndLabels, counts.getPMLabel()) foldersAndLabels = append(foldersAndLabels, counts.getPMLabel())

View File

@ -36,23 +36,41 @@ import (
func (storeMailbox *Mailbox) GetAPIIDsFromUIDRange(start, stop uint32) (apiIDs []string, err error) { func (storeMailbox *Mailbox) GetAPIIDsFromUIDRange(start, stop uint32) (apiIDs []string, err error) {
err = storeMailbox.db().View(func(tx *bolt.Tx) error { err = storeMailbox.db().View(func(tx *bolt.Tx) error {
b := storeMailbox.txGetIMAPIDsBucket(tx) b := storeMailbox.txGetIMAPIDsBucket(tx)
c := b.Cursor()
// GODT-1153 If the mailbox is empty we should reply BAD to client.
if uid, _ := c.Last(); uid == nil {
return nil
}
// If the start range is a wildcard, the range can only refer to the last message in the mailbox.
if start == 0 {
_, apiID := c.Last()
apiIDs = append(apiIDs, string(apiID))
return nil
}
// Resolve the stop value to be the final UID in the mailbox.
if stop == 0 { if stop == 0 {
// A null stop means no stop. stop = storeMailbox.txGetFinalUID(b)
stop = ^uint32(0) }
// After resolving the stop value, it might be less than start so we sort it.
if start > stop {
start, stop = stop, start
} }
startb := itob(start) startb := itob(start)
stopb := itob(stop) stopb := itob(stop)
c := b.Cursor()
for k, v := c.Seek(startb); k != nil && bytes.Compare(k, stopb) <= 0; k, v = c.Next() { for k, v := c.Seek(startb); k != nil && bytes.Compare(k, stopb) <= 0; k, v = c.Next() {
apiIDs = append(apiIDs, string(v)) apiIDs = append(apiIDs, string(v))
} }
return nil return nil
}) })
return
return apiIDs, err
} }
// GetAPIIDsFromSequenceRange returns API IDs by IMAP sequence number range. // GetAPIIDsFromSequenceRange returns API IDs by IMAP sequence number range.
@ -60,28 +78,52 @@ func (storeMailbox *Mailbox) GetAPIIDsFromSequenceRange(start, stop uint32) (api
err = storeMailbox.db().View(func(tx *bolt.Tx) error { err = storeMailbox.db().View(func(tx *bolt.Tx) error {
b := storeMailbox.txGetIMAPIDsBucket(tx) b := storeMailbox.txGetIMAPIDsBucket(tx)
c := b.Cursor() c := b.Cursor()
// GODT-1153 If the mailbox is empty we should reply BAD to client.
if uid, _ := c.Last(); uid == nil {
return nil
}
// If the start range is a wildcard, the range can only refer to the last message in the mailbox.
if start == 0 {
_, apiID := c.Last()
apiIDs = append(apiIDs, string(apiID))
return nil
}
var i uint32 var i uint32
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
i++ i++
if i < start { if i < start {
continue continue
} }
if stop > 0 && i > stop { if stop > 0 && i > stop {
break break
} }
apiIDs = append(apiIDs, string(v)) apiIDs = append(apiIDs, string(v))
} }
if stop == 0 && len(apiIDs) == 0 {
if _, apiID := c.Last(); len(apiID) > 0 {
apiIDs = append(apiIDs, string(apiID))
}
}
return nil return nil
}) })
return
return apiIDs, err
} }
// GetLatestAPIID returns the latest message API ID which still exists. // GetLatestAPIID returns the latest message API ID which still exists.
// Info: not the latest IMAP UID which can be already removed. // Info: not the latest IMAP UID which can be already removed.
func (storeMailbox *Mailbox) GetLatestAPIID() (apiID string, err error) { func (storeMailbox *Mailbox) GetLatestAPIID() (apiID string, err error) {
err = storeMailbox.db().View(func(tx *bolt.Tx) error { err = storeMailbox.db().View(func(tx *bolt.Tx) error {
b := storeMailbox.txGetAPIIDsBucket(tx) c := storeMailbox.txGetAPIIDsBucket(tx).Cursor()
c := b.Cursor()
lastAPIID, _ := c.Last() lastAPIID, _ := c.Last()
apiID = string(lastAPIID) apiID = string(lastAPIID)
if apiID == "" { if apiID == "" {
@ -283,3 +325,13 @@ func (storeMailbox *Mailbox) GetUIDByHeader(header *mail.Header) (foundUID uint3
return foundUID return foundUID
} }
func (storeMailbox *Mailbox) txGetFinalUID(b *bolt.Bucket) uint32 {
uid, _ := b.Cursor().Last()
if uid == nil {
panic(errors.New("cannot get final UID of empty mailbox"))
}
return btoi(uid)
}

View File

@ -37,10 +37,10 @@ func TestGetSequenceNumberAndGetUID(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel})
insertMessage(t, m, "msg3", "Test message 3", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg3", "Test message 3", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg4", "Test message 4", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg4", "Test message 4", addrID1, false, []string{pmapi.AllMailLabel})
checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"}) checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"})
@ -56,7 +56,7 @@ func checkMailboxMessageIDs(t *testing.T, m *mocksForStore, mailboxLabel string,
storeAddress := m.store.addresses[addrID1] storeAddress := m.store.addresses[addrID1]
storeMailbox := storeAddress.mailboxes[mailboxLabel] storeMailbox := storeAddress.mailboxes[mailboxLabel]
ids, err := storeMailbox.GetAPIIDsFromSequenceRange(0, uint32(len(wantIDs))) ids, err := storeMailbox.GetAPIIDsFromSequenceRange(1, uint32(len(wantIDs)))
require.Nil(t, err) require.Nil(t, err)
idx := 0 idx := 0
@ -82,20 +82,20 @@ func TestGetUIDByHeader(t *testing.T) { //nolint[funlen]
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
tstMsg := getTestMessage("msg1", "Without external ID", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.SentLabel}) tstMsg := getTestMessage("msg1", "Without external ID", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel})
require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg))
tstMsg = getTestMessage("msg2", "External ID with spaces", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.SentLabel}) tstMsg = getTestMessage("msg2", "External ID with spaces", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel})
tstMsg.ExternalID = " externalID-non-pm-com " tstMsg.ExternalID = " externalID-non-pm-com "
require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg))
tstMsg = getTestMessage("msg3", "External ID with <>", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.SentLabel}) tstMsg = getTestMessage("msg3", "External ID with <>", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel})
tstMsg.ExternalID = "<externalID@pm.me>" tstMsg.ExternalID = "<externalID@pm.me>"
tstMsg.Header = mail.Header{"References": []string{"wrongID", "externalID-non-pm-com", "msg2"}} tstMsg.Header = mail.Header{"References": []string{"wrongID", "externalID-non-pm-com", "msg2"}}
require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg))
// Not sure if this is a real-world scenario but we should be able to address this properly. // Not sure if this is a real-world scenario but we should be able to address this properly.
tstMsg = getTestMessage("msg4", "External ID with <> and spaces and special characters", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.SentLabel}) tstMsg = getTestMessage("msg4", "External ID with <> and spaces and special characters", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel})
tstMsg.ExternalID = " < external.()+*[]ID@another.pm.me > " tstMsg.ExternalID = " < external.()+*[]ID@another.pm.me > "
require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg))

View File

@ -41,16 +41,14 @@ 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.client().GetMessage(apiID) msg, err := storeMailbox.client().GetMessage(exposeContextForIMAP(), apiID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newStoreMessage(storeMailbox, msg), nil return newStoreMessage(storeMailbox, msg), nil
} }
// ImportMessage imports the message by calling an API. func (storeMailbox *Mailbox) ImportMessage(enc []byte, seen bool, labelIDs []string, flags, time int64) (string, error) {
// It has to be propagated to all mailboxes which is done by the event loop.
func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error {
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
if storeMailbox.labelID != pmapi.AllMailLabel { if storeMailbox.labelID != pmapi.AllMailLabel {
@ -58,23 +56,26 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe
} }
importReqs := &pmapi.ImportMsgReq{ importReqs := &pmapi.ImportMsgReq{
AddressID: msg.AddressID, Metadata: &pmapi.ImportMetadata{
Body: body, AddressID: storeMailbox.storeAddress.addressID,
Unread: msg.Unread, Unread: pmapi.Boolean(!seen),
Flags: msg.Flags, Flags: flags,
Time: msg.Time, Time: time,
LabelIDs: labelIDs, LabelIDs: labelIDs,
},
Message: append(enc, "\r\n"...),
} }
res, err := storeMailbox.client().Import([]*pmapi.ImportMsgReq{importReqs}) res, err := storeMailbox.client().Import(exposeContextForIMAP(), pmapi.ImportMsgReqs{importReqs})
if err != nil { if err != nil {
return err return "", err
} }
if len(res) == 0 { if len(res) == 0 {
return errors.New("no import response") return "", errors.New("no import response")
} }
msg.ID = res[0].MessageID
return res[0].Error return res[0].MessageID, res[0].Error
} }
// LabelMessages adds the label by calling an API. // LabelMessages adds the label by calling an API.
@ -95,7 +96,7 @@ func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error {
return ErrAllMailOpNotAllowed return ErrAllMailOpNotAllowed
} }
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.client().LabelMessages(apiIDs, storeMailbox.labelID) return storeMailbox.client().LabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID)
} }
// UnlabelMessages removes the label by calling an API. // UnlabelMessages removes the label by calling an API.
@ -108,7 +109,7 @@ func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error {
return ErrAllMailOpNotAllowed return ErrAllMailOpNotAllowed
} }
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID) return storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID)
} }
// MarkMessagesRead marks the message read by calling an API. // MarkMessagesRead marks the message read by calling an API.
@ -128,14 +129,14 @@ func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error {
// Therefore we do not issue API update if the message is already read. // Therefore we do not issue API update if the message is already read.
ids := []string{} ids := []string{}
for _, apiID := range apiIDs { for _, apiID := range apiIDs {
if message, _ := storeMailbox.store.getMessageFromDB(apiID); message == nil || message.Unread == 1 { if message, _ := storeMailbox.store.getMessageFromDB(apiID); message == nil || message.Unread {
ids = append(ids, apiID) ids = append(ids, apiID)
} }
} }
if len(ids) == 0 { if len(ids) == 0 {
return nil return nil
} }
return storeMailbox.client().MarkMessagesRead(ids) return storeMailbox.client().MarkMessagesRead(exposeContextForIMAP(), ids)
} }
// MarkMessagesUnread marks the message unread by calling an API. // MarkMessagesUnread marks the message unread by calling an API.
@ -147,7 +148,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.client().MarkMessagesUnread(apiIDs) return storeMailbox.client().MarkMessagesUnread(exposeContextForIMAP(), apiIDs)
} }
// MarkMessagesStarred adds the Starred label by calling an API. // MarkMessagesStarred adds the Starred label by calling an API.
@ -160,7 +161,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.client().LabelMessages(apiIDs, pmapi.StarredLabel) return storeMailbox.client().LabelMessages(exposeContextForIMAP(), apiIDs, pmapi.StarredLabel)
} }
// MarkMessagesUnstarred removes the Starred label by calling an API. // MarkMessagesUnstarred removes the Starred label by calling an API.
@ -173,7 +174,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.client().UnlabelMessages(apiIDs, pmapi.StarredLabel) return storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, pmapi.StarredLabel)
} }
// MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API // MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API
@ -257,11 +258,11 @@ func (storeMailbox *Mailbox) RemoveDeleted(apiIDs []string) error {
} }
case pmapi.DraftLabel: case pmapi.DraftLabel:
storeMailbox.log.WithField("ids", apiIDs).Warn("Deleting drafts") storeMailbox.log.WithField("ids", apiIDs).Warn("Deleting drafts")
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil { if err := storeMailbox.client().DeleteMessages(exposeContextForIMAP(), apiIDs); err != nil {
return err return err
} }
default: default:
if err := storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID); err != nil { if err := storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID); err != nil {
return err return err
} }
} }
@ -299,13 +300,13 @@ func (storeMailbox *Mailbox) deleteFromTrashOrSpam(apiIDs []string) error {
} }
} }
if len(messageIDsToUnlabel) > 0 { if len(messageIDsToUnlabel) > 0 {
if err := storeMailbox.client().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil { if err := storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), messageIDsToUnlabel, storeMailbox.labelID); err != nil {
l.WithError(err).Warning("Cannot unlabel before deleting") l.WithError(err).Warning("Cannot unlabel before deleting")
} }
} }
if len(messageIDsToDelete) > 0 { if len(messageIDsToDelete) > 0 {
storeMailbox.log.WithField("ids", messageIDsToDelete).Warn("Deleting messages") storeMailbox.log.WithField("ids", messageIDsToDelete).Warn("Deleting messages")
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil { if err := storeMailbox.client().DeleteMessages(exposeContextForIMAP(), messageIDsToDelete); err != nil {
return err return err
} }
} }
@ -353,6 +354,10 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
// Buckets are not initialized right away because it's a heavy operation. // Buckets are not initialized right away because it's a heavy operation.
// The best option is to get the same bucket only once and only when needed. // The best option is to get the same bucket only once and only when needed.
var apiBucket, imapBucket, deletedBucket *bolt.Bucket var apiBucket, imapBucket, deletedBucket *bolt.Bucket
// Collect updates to send them later, after possibly sending the status/EXISTS update.
updates := make([]func(), 0, len(msgs))
for _, msg := range msgs { for _, msg := range msgs {
if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) { if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) {
continue continue
@ -415,14 +420,18 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
if err != nil { if err != nil {
return errors.Wrap(err, "cannot get sequence number from UID") return errors.Wrap(err, "cannot get sequence number from UID")
} }
storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address, updates = append(updates, func() {
storeMailbox.labelName, storeMailbox.store.notifyUpdateMessage(
uid, storeMailbox.storeAddress.address,
seqNum, storeMailbox.labelName,
msg, uid,
false, // new message is never marked as deleted seqNum,
) msg,
false, // new message is never marked as deleted
)
})
shouldSendMailboxUpdate = true shouldSendMailboxUpdate = true
} }
@ -432,6 +441,10 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
} }
} }
for _, update := range updates {
update()
}
return nil return nil
} }

View File

@ -25,6 +25,7 @@ import (
pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
@ -153,15 +154,27 @@ func (message *Message) getRawHeader() (raw []byte, err error) {
} }
// GetHeader will return cached header from DB. // GetHeader will return cached header from DB.
func (message *Message) GetHeader() textproto.MIMEHeader { func (message *Message) GetHeader() []byte {
raw, err := message.getRawHeader() raw, err := message.getRawHeader()
if err != nil && raw == nil { if err != nil {
return textproto.MIMEHeader(message.msg.Header) panic(errors.Wrap(err, "failed to get raw message header"))
} }
return raw
}
// GetMIMEHeader will return cached header from DB, parsed as a textproto.MIMEHeader.
func (message *Message) GetMIMEHeader() textproto.MIMEHeader {
raw, err := message.getRawHeader()
if err != nil {
panic(errors.Wrap(err, "failed to get raw message header"))
}
header, err := textproto.NewReader(bufio.NewReader(bytes.NewReader(raw))).ReadMIMEHeader() header, err := textproto.NewReader(bufio.NewReader(bytes.NewReader(raw))).ReadMIMEHeader()
if err != nil { if err != nil {
return textproto.MIMEHeader(message.msg.Header) return textproto.MIMEHeader(message.msg.Header)
} }
return header return header
} }

View File

@ -1,136 +1,100 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/store (interfaces: PanicHandler,ClientManager,BridgeUser,ChangeNotifier) // Source: github.com/ProtonMail/proton-bridge/internal/store (interfaces: PanicHandler,BridgeUser,ChangeNotifier)
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
import ( import (
context "context"
reflect "reflect" reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )
// MockPanicHandler is a mock of PanicHandler interface // MockPanicHandler is a mock of PanicHandler interface.
type MockPanicHandler struct { type MockPanicHandler struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder recorder *MockPanicHandlerMockRecorder
} }
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler // MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
type MockPanicHandlerMockRecorder struct { type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler mock *MockPanicHandler
} }
// NewMockPanicHandler creates a new mock instance // NewMockPanicHandler creates a new mock instance.
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler { func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl} mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock} mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder { func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder return m.recorder
} }
// HandlePanic mocks base method // HandlePanic mocks base method.
func (m *MockPanicHandler) HandlePanic() { func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic") m.ctrl.Call(m, "HandlePanic")
} }
// HandlePanic indicates an expected call of HandlePanic // HandlePanic indicates an expected call of HandlePanic.
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call { func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
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 // MockBridgeUser is a mock of BridgeUser interface.
type MockClientManager struct {
ctrl *gomock.Controller
recorder *MockClientManagerMockRecorder
}
// MockClientManagerMockRecorder is the mock recorder for MockClientManager
type MockClientManagerMockRecorder struct {
mock *MockClientManager
}
// NewMockClientManager creates a new mock instance
func NewMockClientManager(ctrl *gomock.Controller) *MockClientManager {
mock := &MockClientManager{ctrl: ctrl}
mock.recorder = &MockClientManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockClientManager) EXPECT() *MockClientManagerMockRecorder {
return m.recorder
}
// GetClient mocks base method
func (m *MockClientManager) GetClient(arg0 string) pmapi.Client {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClient", arg0)
ret0, _ := ret[0].(pmapi.Client)
return ret0
}
// GetClient indicates an expected call of GetClient
func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
}
// MockBridgeUser is a mock of BridgeUser interface
type MockBridgeUser struct { type MockBridgeUser struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockBridgeUserMockRecorder recorder *MockBridgeUserMockRecorder
} }
// MockBridgeUserMockRecorder is the mock recorder for MockBridgeUser // MockBridgeUserMockRecorder is the mock recorder for MockBridgeUser.
type MockBridgeUserMockRecorder struct { type MockBridgeUserMockRecorder struct {
mock *MockBridgeUser mock *MockBridgeUser
} }
// NewMockBridgeUser creates a new mock instance // NewMockBridgeUser creates a new mock instance.
func NewMockBridgeUser(ctrl *gomock.Controller) *MockBridgeUser { func NewMockBridgeUser(ctrl *gomock.Controller) *MockBridgeUser {
mock := &MockBridgeUser{ctrl: ctrl} mock := &MockBridgeUser{ctrl: ctrl}
mock.recorder = &MockBridgeUserMockRecorder{mock} mock.recorder = &MockBridgeUserMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBridgeUser) EXPECT() *MockBridgeUserMockRecorder { func (m *MockBridgeUser) EXPECT() *MockBridgeUserMockRecorder {
return m.recorder return m.recorder
} }
// CloseAllConnections mocks base method // CloseAllConnections mocks base method.
func (m *MockBridgeUser) CloseAllConnections() { func (m *MockBridgeUser) CloseAllConnections() {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "CloseAllConnections") m.ctrl.Call(m, "CloseAllConnections")
} }
// CloseAllConnections indicates an expected call of CloseAllConnections // CloseAllConnections indicates an expected call of CloseAllConnections.
func (mr *MockBridgeUserMockRecorder) CloseAllConnections() *gomock.Call { func (mr *MockBridgeUserMockRecorder) CloseAllConnections() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseAllConnections", reflect.TypeOf((*MockBridgeUser)(nil).CloseAllConnections)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseAllConnections", reflect.TypeOf((*MockBridgeUser)(nil).CloseAllConnections))
} }
// CloseConnection mocks base method // CloseConnection mocks base method.
func (m *MockBridgeUser) CloseConnection(arg0 string) { func (m *MockBridgeUser) CloseConnection(arg0 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "CloseConnection", arg0) m.ctrl.Call(m, "CloseConnection", arg0)
} }
// CloseConnection indicates an expected call of CloseConnection // CloseConnection indicates an expected call of CloseConnection.
func (mr *MockBridgeUserMockRecorder) CloseConnection(arg0 interface{}) *gomock.Call { func (mr *MockBridgeUserMockRecorder) CloseConnection(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseConnection", reflect.TypeOf((*MockBridgeUser)(nil).CloseConnection), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseConnection", reflect.TypeOf((*MockBridgeUser)(nil).CloseConnection), arg0)
} }
// GetAddressID mocks base method // GetAddressID mocks base method.
func (m *MockBridgeUser) GetAddressID(arg0 string) (string, error) { func (m *MockBridgeUser) GetAddressID(arg0 string) (string, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAddressID", arg0) ret := m.ctrl.Call(m, "GetAddressID", arg0)
@ -139,13 +103,27 @@ func (m *MockBridgeUser) GetAddressID(arg0 string) (string, error) {
return ret0, ret1 return ret0, ret1
} }
// GetAddressID indicates an expected call of GetAddressID // GetAddressID indicates an expected call of GetAddressID.
func (mr *MockBridgeUserMockRecorder) GetAddressID(arg0 interface{}) *gomock.Call { func (mr *MockBridgeUserMockRecorder) GetAddressID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAddressID", reflect.TypeOf((*MockBridgeUser)(nil).GetAddressID), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAddressID", reflect.TypeOf((*MockBridgeUser)(nil).GetAddressID), arg0)
} }
// GetPrimaryAddress mocks base method // GetClient mocks base method.
func (m *MockBridgeUser) GetClient() pmapi.Client {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClient")
ret0, _ := ret[0].(pmapi.Client)
return ret0
}
// GetClient indicates an expected call of GetClient.
func (mr *MockBridgeUserMockRecorder) GetClient() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockBridgeUser)(nil).GetClient))
}
// GetPrimaryAddress mocks base method.
func (m *MockBridgeUser) GetPrimaryAddress() string { func (m *MockBridgeUser) GetPrimaryAddress() string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPrimaryAddress") ret := m.ctrl.Call(m, "GetPrimaryAddress")
@ -153,13 +131,13 @@ func (m *MockBridgeUser) GetPrimaryAddress() string {
return ret0 return ret0
} }
// GetPrimaryAddress indicates an expected call of GetPrimaryAddress // GetPrimaryAddress indicates an expected call of GetPrimaryAddress.
func (mr *MockBridgeUserMockRecorder) GetPrimaryAddress() *gomock.Call { func (mr *MockBridgeUserMockRecorder) GetPrimaryAddress() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrimaryAddress", reflect.TypeOf((*MockBridgeUser)(nil).GetPrimaryAddress)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrimaryAddress", reflect.TypeOf((*MockBridgeUser)(nil).GetPrimaryAddress))
} }
// GetStoreAddresses mocks base method // GetStoreAddresses mocks base method.
func (m *MockBridgeUser) GetStoreAddresses() []string { func (m *MockBridgeUser) GetStoreAddresses() []string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStoreAddresses") ret := m.ctrl.Call(m, "GetStoreAddresses")
@ -167,13 +145,13 @@ func (m *MockBridgeUser) GetStoreAddresses() []string {
return ret0 return ret0
} }
// GetStoreAddresses indicates an expected call of GetStoreAddresses // GetStoreAddresses indicates an expected call of GetStoreAddresses.
func (mr *MockBridgeUserMockRecorder) GetStoreAddresses() *gomock.Call { func (mr *MockBridgeUserMockRecorder) GetStoreAddresses() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStoreAddresses", reflect.TypeOf((*MockBridgeUser)(nil).GetStoreAddresses)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStoreAddresses", reflect.TypeOf((*MockBridgeUser)(nil).GetStoreAddresses))
} }
// ID mocks base method // ID mocks base method.
func (m *MockBridgeUser) ID() string { func (m *MockBridgeUser) ID() string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ID") ret := m.ctrl.Call(m, "ID")
@ -181,13 +159,13 @@ func (m *MockBridgeUser) ID() string {
return ret0 return ret0
} }
// ID indicates an expected call of ID // ID indicates an expected call of ID.
func (mr *MockBridgeUserMockRecorder) ID() *gomock.Call { func (mr *MockBridgeUserMockRecorder) ID() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockBridgeUser)(nil).ID)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockBridgeUser)(nil).ID))
} }
// IsCombinedAddressMode mocks base method // IsCombinedAddressMode mocks base method.
func (m *MockBridgeUser) IsCombinedAddressMode() bool { func (m *MockBridgeUser) IsCombinedAddressMode() bool {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsCombinedAddressMode") ret := m.ctrl.Call(m, "IsCombinedAddressMode")
@ -195,13 +173,13 @@ func (m *MockBridgeUser) IsCombinedAddressMode() bool {
return ret0 return ret0
} }
// IsCombinedAddressMode indicates an expected call of IsCombinedAddressMode // IsCombinedAddressMode indicates an expected call of IsCombinedAddressMode.
func (mr *MockBridgeUserMockRecorder) IsCombinedAddressMode() *gomock.Call { func (mr *MockBridgeUserMockRecorder) IsCombinedAddressMode() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCombinedAddressMode", reflect.TypeOf((*MockBridgeUser)(nil).IsCombinedAddressMode)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCombinedAddressMode", reflect.TypeOf((*MockBridgeUser)(nil).IsCombinedAddressMode))
} }
// IsConnected mocks base method // IsConnected mocks base method.
func (m *MockBridgeUser) IsConnected() bool { func (m *MockBridgeUser) IsConnected() bool {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsConnected") ret := m.ctrl.Call(m, "IsConnected")
@ -209,13 +187,13 @@ func (m *MockBridgeUser) IsConnected() bool {
return ret0 return ret0
} }
// IsConnected indicates an expected call of IsConnected // IsConnected indicates an expected call of IsConnected.
func (mr *MockBridgeUserMockRecorder) IsConnected() *gomock.Call { func (mr *MockBridgeUserMockRecorder) IsConnected() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockBridgeUser)(nil).IsConnected)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockBridgeUser)(nil).IsConnected))
} }
// Logout mocks base method // Logout mocks base method.
func (m *MockBridgeUser) Logout() error { func (m *MockBridgeUser) Logout() error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout") ret := m.ctrl.Call(m, "Logout")
@ -223,50 +201,50 @@ func (m *MockBridgeUser) Logout() error {
return ret0 return ret0
} }
// Logout indicates an expected call of Logout // Logout indicates an expected call of Logout.
func (mr *MockBridgeUserMockRecorder) Logout() *gomock.Call { func (mr *MockBridgeUserMockRecorder) Logout() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockBridgeUser)(nil).Logout)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockBridgeUser)(nil).Logout))
} }
// UpdateUser mocks base method // UpdateUser mocks base method.
func (m *MockBridgeUser) UpdateUser() error { func (m *MockBridgeUser) UpdateUser(arg0 context.Context) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser") ret := m.ctrl.Call(m, "UpdateUser", arg0)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// UpdateUser indicates an expected call of UpdateUser // UpdateUser indicates an expected call of UpdateUser.
func (mr *MockBridgeUserMockRecorder) UpdateUser() *gomock.Call { func (mr *MockBridgeUserMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockBridgeUser)(nil).UpdateUser)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockBridgeUser)(nil).UpdateUser), arg0)
} }
// MockChangeNotifier is a mock of ChangeNotifier interface // MockChangeNotifier is a mock of ChangeNotifier interface.
type MockChangeNotifier struct { type MockChangeNotifier struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockChangeNotifierMockRecorder recorder *MockChangeNotifierMockRecorder
} }
// MockChangeNotifierMockRecorder is the mock recorder for MockChangeNotifier // MockChangeNotifierMockRecorder is the mock recorder for MockChangeNotifier.
type MockChangeNotifierMockRecorder struct { type MockChangeNotifierMockRecorder struct {
mock *MockChangeNotifier mock *MockChangeNotifier
} }
// NewMockChangeNotifier creates a new mock instance // NewMockChangeNotifier creates a new mock instance.
func NewMockChangeNotifier(ctrl *gomock.Controller) *MockChangeNotifier { func NewMockChangeNotifier(ctrl *gomock.Controller) *MockChangeNotifier {
mock := &MockChangeNotifier{ctrl: ctrl} mock := &MockChangeNotifier{ctrl: ctrl}
mock.recorder = &MockChangeNotifierMockRecorder{mock} mock.recorder = &MockChangeNotifierMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockChangeNotifier) EXPECT() *MockChangeNotifierMockRecorder { func (m *MockChangeNotifier) EXPECT() *MockChangeNotifierMockRecorder {
return m.recorder return m.recorder
} }
// CanDelete mocks base method // CanDelete mocks base method.
func (m *MockChangeNotifier) CanDelete(arg0 string) (bool, func()) { func (m *MockChangeNotifier) CanDelete(arg0 string) (bool, func()) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CanDelete", arg0) ret := m.ctrl.Call(m, "CanDelete", arg0)
@ -275,67 +253,67 @@ func (m *MockChangeNotifier) CanDelete(arg0 string) (bool, func()) {
return ret0, ret1 return ret0, ret1
} }
// CanDelete indicates an expected call of CanDelete // CanDelete indicates an expected call of CanDelete.
func (mr *MockChangeNotifierMockRecorder) CanDelete(arg0 interface{}) *gomock.Call { func (mr *MockChangeNotifierMockRecorder) CanDelete(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanDelete", reflect.TypeOf((*MockChangeNotifier)(nil).CanDelete), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanDelete", reflect.TypeOf((*MockChangeNotifier)(nil).CanDelete), arg0)
} }
// DeleteMessage mocks base method // DeleteMessage mocks base method.
func (m *MockChangeNotifier) DeleteMessage(arg0, arg1 string, arg2 uint32) { func (m *MockChangeNotifier) DeleteMessage(arg0, arg1 string, arg2 uint32) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "DeleteMessage", arg0, arg1, arg2) m.ctrl.Call(m, "DeleteMessage", arg0, arg1, arg2)
} }
// DeleteMessage indicates an expected call of DeleteMessage // DeleteMessage indicates an expected call of DeleteMessage.
func (mr *MockChangeNotifierMockRecorder) DeleteMessage(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockChangeNotifierMockRecorder) DeleteMessage(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockChangeNotifier)(nil).DeleteMessage), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockChangeNotifier)(nil).DeleteMessage), arg0, arg1, arg2)
} }
// MailboxCreated mocks base method // MailboxCreated mocks base method.
func (m *MockChangeNotifier) MailboxCreated(arg0, arg1 string) { func (m *MockChangeNotifier) MailboxCreated(arg0, arg1 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "MailboxCreated", arg0, arg1) m.ctrl.Call(m, "MailboxCreated", arg0, arg1)
} }
// MailboxCreated indicates an expected call of MailboxCreated // MailboxCreated indicates an expected call of MailboxCreated.
func (mr *MockChangeNotifierMockRecorder) MailboxCreated(arg0, arg1 interface{}) *gomock.Call { func (mr *MockChangeNotifierMockRecorder) MailboxCreated(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxCreated", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxCreated), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxCreated", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxCreated), arg0, arg1)
} }
// MailboxStatus mocks base method // MailboxStatus mocks base method.
func (m *MockChangeNotifier) MailboxStatus(arg0, arg1 string, arg2, arg3, arg4 uint32) { func (m *MockChangeNotifier) MailboxStatus(arg0, arg1 string, arg2, arg3, arg4 uint32) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "MailboxStatus", arg0, arg1, arg2, arg3, arg4) m.ctrl.Call(m, "MailboxStatus", arg0, arg1, arg2, arg3, arg4)
} }
// MailboxStatus indicates an expected call of MailboxStatus // MailboxStatus indicates an expected call of MailboxStatus.
func (mr *MockChangeNotifierMockRecorder) MailboxStatus(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { func (mr *MockChangeNotifierMockRecorder) MailboxStatus(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxStatus", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxStatus), arg0, arg1, arg2, arg3, arg4) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxStatus", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxStatus), arg0, arg1, arg2, arg3, arg4)
} }
// Notice mocks base method // Notice mocks base method.
func (m *MockChangeNotifier) Notice(arg0, arg1 string) { func (m *MockChangeNotifier) Notice(arg0, arg1 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "Notice", arg0, arg1) m.ctrl.Call(m, "Notice", arg0, arg1)
} }
// Notice indicates an expected call of Notice // Notice indicates an expected call of Notice.
func (mr *MockChangeNotifierMockRecorder) Notice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockChangeNotifierMockRecorder) Notice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notice", reflect.TypeOf((*MockChangeNotifier)(nil).Notice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notice", reflect.TypeOf((*MockChangeNotifier)(nil).Notice), arg0, arg1)
} }
// UpdateMessage mocks base method // UpdateMessage mocks base method.
func (m *MockChangeNotifier) UpdateMessage(arg0, arg1 string, arg2, arg3 uint32, arg4 *pmapi.Message, arg5 bool) { func (m *MockChangeNotifier) UpdateMessage(arg0, arg1 string, arg2, arg3 uint32, arg4 *pmapi.Message, arg5 bool) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "UpdateMessage", arg0, arg1, arg2, arg3, arg4, arg5) m.ctrl.Call(m, "UpdateMessage", arg0, arg1, arg2, arg3, arg4, arg5)
} }
// UpdateMessage indicates an expected call of UpdateMessage // UpdateMessage indicates an expected call of UpdateMessage.
func (mr *MockChangeNotifierMockRecorder) UpdateMessage(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { func (mr *MockChangeNotifierMockRecorder) UpdateMessage(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMessage", reflect.TypeOf((*MockChangeNotifier)(nil).UpdateMessage), arg0, arg1, arg2, arg3, arg4, arg5) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMessage", reflect.TypeOf((*MockChangeNotifier)(nil).UpdateMessage), arg0, arg1, arg2, arg3, arg4, arg5)

View File

@ -11,96 +11,110 @@ import (
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )
// MockListener is a mock of Listener interface // MockListener is a mock of Listener interface.
type MockListener struct { type MockListener struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockListenerMockRecorder recorder *MockListenerMockRecorder
} }
// MockListenerMockRecorder is the mock recorder for MockListener // MockListenerMockRecorder is the mock recorder for MockListener.
type MockListenerMockRecorder struct { type MockListenerMockRecorder struct {
mock *MockListener mock *MockListener
} }
// NewMockListener creates a new mock instance // NewMockListener creates a new mock instance.
func NewMockListener(ctrl *gomock.Controller) *MockListener { func NewMockListener(ctrl *gomock.Controller) *MockListener {
mock := &MockListener{ctrl: ctrl} mock := &MockListener{ctrl: ctrl}
mock.recorder = &MockListenerMockRecorder{mock} mock.recorder = &MockListenerMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockListener) EXPECT() *MockListenerMockRecorder { func (m *MockListener) EXPECT() *MockListenerMockRecorder {
return m.recorder return m.recorder
} }
// Add mocks base method // Add mocks base method.
func (m *MockListener) Add(arg0 string, arg1 chan<- string) { func (m *MockListener) Add(arg0 string, arg1 chan<- string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "Add", arg0, arg1) m.ctrl.Call(m, "Add", arg0, arg1)
} }
// Add indicates an expected call of Add // Add indicates an expected call of Add.
func (mr *MockListenerMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call { func (mr *MockListenerMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockListener)(nil).Add), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockListener)(nil).Add), arg0, arg1)
} }
// Emit mocks base method // Emit mocks base method.
func (m *MockListener) Emit(arg0, arg1 string) { func (m *MockListener) Emit(arg0, arg1 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "Emit", arg0, arg1) m.ctrl.Call(m, "Emit", arg0, arg1)
} }
// Emit indicates an expected call of Emit // Emit indicates an expected call of Emit.
func (mr *MockListenerMockRecorder) Emit(arg0, arg1 interface{}) *gomock.Call { func (mr *MockListenerMockRecorder) Emit(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), arg0, arg1)
} }
// Remove mocks base method // ProvideChannel mocks base method.
func (m *MockListener) ProvideChannel(arg0 string) <-chan string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ProvideChannel", arg0)
ret0, _ := ret[0].(<-chan string)
return ret0
}
// ProvideChannel indicates an expected call of ProvideChannel.
func (mr *MockListenerMockRecorder) ProvideChannel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProvideChannel", reflect.TypeOf((*MockListener)(nil).ProvideChannel), arg0)
}
// Remove mocks base method.
func (m *MockListener) Remove(arg0 string, arg1 chan<- string) { func (m *MockListener) Remove(arg0 string, arg1 chan<- string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "Remove", arg0, arg1) m.ctrl.Call(m, "Remove", arg0, arg1)
} }
// Remove indicates an expected call of Remove // Remove indicates an expected call of Remove.
func (mr *MockListenerMockRecorder) Remove(arg0, arg1 interface{}) *gomock.Call { func (mr *MockListenerMockRecorder) Remove(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockListener)(nil).Remove), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockListener)(nil).Remove), arg0, arg1)
} }
// RetryEmit mocks base method // RetryEmit mocks base method.
func (m *MockListener) RetryEmit(arg0 string) { func (m *MockListener) RetryEmit(arg0 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "RetryEmit", arg0) m.ctrl.Call(m, "RetryEmit", arg0)
} }
// RetryEmit indicates an expected call of RetryEmit // RetryEmit indicates an expected call of RetryEmit.
func (mr *MockListenerMockRecorder) RetryEmit(arg0 interface{}) *gomock.Call { func (mr *MockListenerMockRecorder) RetryEmit(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryEmit", reflect.TypeOf((*MockListener)(nil).RetryEmit), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryEmit", reflect.TypeOf((*MockListener)(nil).RetryEmit), arg0)
} }
// SetBuffer mocks base method // SetBuffer mocks base method.
func (m *MockListener) SetBuffer(arg0 string) { func (m *MockListener) SetBuffer(arg0 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "SetBuffer", arg0) m.ctrl.Call(m, "SetBuffer", arg0)
} }
// SetBuffer indicates an expected call of SetBuffer // SetBuffer indicates an expected call of SetBuffer.
func (mr *MockListenerMockRecorder) SetBuffer(arg0 interface{}) *gomock.Call { func (mr *MockListenerMockRecorder) SetBuffer(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBuffer", reflect.TypeOf((*MockListener)(nil).SetBuffer), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBuffer", reflect.TypeOf((*MockListener)(nil).SetBuffer), arg0)
} }
// SetLimit mocks base method // SetLimit mocks base method.
func (m *MockListener) SetLimit(arg0 string, arg1 time.Duration) { func (m *MockListener) SetLimit(arg0 string, arg1 time.Duration) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "SetLimit", arg0, arg1) m.ctrl.Call(m, "SetLimit", arg0, arg1)
} }
// SetLimit indicates an expected call of SetLimit // SetLimit indicates an expected call of SetLimit.
func (mr *MockListenerMockRecorder) SetLimit(arg0, arg1 interface{}) *gomock.Call { func (mr *MockListenerMockRecorder) SetLimit(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockListener)(nil).SetLimit), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockListener)(nil).SetLimit), arg0, arg1)

View File

@ -19,6 +19,7 @@
package store package store
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"sync" "sync"
@ -100,13 +101,24 @@ var (
ErrNoSuchSeqNum = errors.New("no such sequence number") //nolint[gochecknoglobals] ErrNoSuchSeqNum = errors.New("no such sequence number") //nolint[gochecknoglobals]
) )
// exposeContextForIMAP should be replaced once with context passed
// as an argument from IMAP package and IMAP library should cancel
// context when IMAP client cancels the request.
func exposeContextForIMAP() context.Context {
return context.TODO()
}
// exposeContextForSMTP is the same as above but for SMTP.
func exposeContextForSMTP() context.Context {
return context.TODO()
}
// 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 {
sentryReporter *sentry.Reporter sentryReporter *sentry.Reporter
panicHandler PanicHandler panicHandler PanicHandler
eventLoop *eventLoop eventLoop *eventLoop
user BridgeUser user BridgeUser
clientManager ClientManager
log *logrus.Entry log *logrus.Entry
@ -127,13 +139,12 @@ func New( // nolint[funlen]
sentryReporter *sentry.Reporter, sentryReporter *sentry.Reporter,
panicHandler PanicHandler, panicHandler PanicHandler,
user BridgeUser, user BridgeUser,
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 || clientManager == nil || events == nil || cache == nil { if user == nil || events == nil || cache == nil {
return nil, fmt.Errorf("missing parameters - user: %v, api: %v, events: %v, cache: %v", user, clientManager, events, cache) return nil, fmt.Errorf("missing parameters - user: %v, events: %v, cache: %v", user, events, cache)
} }
l := log.WithField("user", user.ID()) l := log.WithField("user", user.ID())
@ -156,7 +167,6 @@ func New( // nolint[funlen]
store = &Store{ store = &Store{
sentryReporter: sentryReporter, sentryReporter: sentryReporter,
panicHandler: panicHandler, panicHandler: panicHandler,
clientManager: clientManager,
user: user, user: user,
cache: cache, cache: cache,
filePath: path, filePath: path,
@ -274,13 +284,13 @@ func (store *Store) init(firstInit bool) (err error) {
} }
func (store *Store) client() pmapi.Client { func (store *Store) client() pmapi.Client {
return store.clientManager.GetClient(store.UserID()) return store.user.GetClient()
} }
// 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.client().ListLabels(); err != nil { if labels, err = store.client().ListLabels(context.Background()); 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

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -25,6 +26,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
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" pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
@ -39,8 +41,92 @@ const (
addr2 = "jamesandmichalarecool@pm.me" addr2 = "jamesandmichalarecool@pm.me"
addrID2 = "jamesandmichalarecool" addrID2 = "jamesandmichalarecool"
testPrivateKeyPassword = "apple"
testPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js v0.7.1
Comment: http://openpgpjs.org
xcMGBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE
WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39
vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi
MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5
c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb
DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB
AAH+CQMIvzcDReuJkc9gnxAkfgmnkBFwRQrqT/4UAPOF8WGVo0uNvDo7Snlk
qWsJS+54+/Xx6Jur/PdBWeEu+6+6GnppYuvsaT0D0nFdFhF6pjng+02IOxfG
qlYXYcW4hRru3BfvJlSvU2LL/Z/ooBnw3T5vqd0eFHKrvabUuwf0x3+K/sru
Fp24rl2PU+bzQlUgKpWzKDmO+0RdKQ6KVCyCDMIXaAkALwNffAvYxI0wnb2y
WAV/bGn1ODnszOYPk3pEMR6kKSxLLaO69kYx4eTERFyJ+1puAxEPCk3Cfeif
yDWi4rU03YB16XH7hQLSFl61SKeIYlkKmkO5Hk1ybi/BhvOGBPVeGGbxWnwI
46G8DfBHW0+uvD5cAQtk2d/q3Ge1I+DIyvuRCcSu0XSBNv/Bkpp4IbAUPBaW
TIvf5p9oxw+AjrMtTtcdSiee1S6CvMMaHhVD7SI6qGA8GqwaXueeLuEXa0Ok
BWlehx8wibMi4a9fLcQZtzJkmGhR1WzXcJfiEg32srILwIzPQYxuFdZZ2elb
gYp/bMEIp4LKhi43IyM6peCDHDzEba8NuOSd0heEqFIm0vlXujMhkyMUvDBv
H0V5On4aMuw/aSEKcAdbazppOru/W1ndyFa5ZHQIC19g72ZaDVyYjPyvNgOV
AFqO4o3IbC5z31zMlTtMbAq2RG9svwUVejn0tmF6UPluTe0U1NuXFpLK6TCH
wqocLz4ecptfJQulpYjClVLgzaYGDuKwQpIwPWg5G/DtKSCGNtEkfqB3aemH
V5xmoYm1v5CQZAEvvsrLA6jxCk9lzqYV8QMivWNXUG+mneIEM35G0HOPzXca
LLyB+N8Zxioc9DPGfdbcxXuVgOKRepbkq4xv1pUpMQ4BUmlkejDRSP+5SIR3
iEthg+FU6GRSQbORE6nhrKjGBk8fpNpozQZVc2VySUTCwHIEEAEIACYFAlRJ
bc8GCwkIBwMCCRA+tiWe3yHfJAQVCAIKAxYCAQIbAwIeAQAA9J0H/RLR/Uwt
CakrPKtfeGaNuOI45SRTNxM8TklC6tM28sJSzkX8qKPzvI1PxyLhs/i0/fCQ
7Z5bU6n41oLuqUt2S9vy+ABlChKAeziOqCHUcMzHOtbKiPkKW88aO687nx+A
ol2XOnMTkVIC+edMUgnKp6tKtZnbO4ea6Cg88TFuli4hLHNXTfCECswuxHOc
AO1OKDRrCd08iPI5CLNCIV60QnduitE1vF6ehgrH25Vl6LEdd8vPVlTYAvsa
6ySk2RIrHNLUZZ3iII3MBFL8HyINp/XA1BQP+QbH801uSLq8agxM4iFT9C+O
D147SawUGhjD5RG7T+YtqItzgA1V9l277EXHwwYEVEltzwEIAJD57uX6bOc4
Tgf3utfL/4hdyoqIMVHkYQOvE27wPsZxX08QsdlaNeGji9Ap2ifIDuckUqn6
Ji9jtZDKtOzdTBm6rnG5nPmkn6BJXPhnecQRP8N0XBISnAGmE4t+bxtts5Wb
qeMdxJYqMiGqzrLBRJEIDTcg3+QF2Y3RywOqlcXqgG/xX++PsvR1Jiz0rEVP
TcBc7ytyb/Av7mx1S802HRYGJHOFtVLoPTrtPCvv+DRDK8JzxQW2XSQLlI0M
9s1tmYhCogYIIqKx9qOTd5mFJ1hJlL6i9xDkvE21qPFASFtww5tiYmUfFaxI
LwbXPZlQ1I/8fuaUdOxctQ+g40ZgHPcAEQEAAf4JAwgdUg8ubE2BT2DITBD+
XFgjrnUlQBilbN8/do/36KHuImSPO/GGLzKh4+oXxrvLc5fQLjeO+bzeen4u
COCBRO0hG7KpJPhQ6+T02uEF6LegE1sEz5hp6BpKUdPZ1+8799Rylb5kubC5
IKnLqqpGDbH3hIsmSV3CG/ESkaGMLc/K0ZPt1JRWtUQ9GesXT0v6fdM5GB/L
cZWFdDoYgZAw5BtymE44knIodfDAYJ4DHnPCh/oilWe1qVTQcNMdtkpBgkuo
THecqEmiODQz5EX8pVmS596XsnPO299Lo3TbaHUQo7EC6Au1Au9+b5hC1pDa
FVCLcproi/Cgch0B/NOCFkVLYmp6BEljRj2dSZRWbO0vgl9kFmJEeiiH41+k
EAI6PASSKZs3BYLFc2I8mBkcvt90kg4MTBjreuk0uWf1hdH2Rv8zprH4h5Uh
gjx5nUDX8WXyeLxTU5EBKry+A2DIe0Gm0/waxp6lBlUl+7ra28KYEoHm8Nq/
N9FCuEhFkFgw6EwUp7jsrFcqBKvmni6jyplm+mJXi3CK+IiNcqub4XPnBI97
lR19fupB/Y6M7yEaxIM8fTQXmP+x/fe8zRphdo+7o+pJQ3hk5LrrNPK8GEZ6
DLDOHjZzROhOgBvWtbxRktHk+f5YpuQL+xWd33IV1xYSSHuoAm0Zwt0QJxBs
oFBwJEq1NWM4FxXJBogvzV7KFhl/hXgtvx+GaMv3y8gucj+gE89xVv0XBXjl
5dy5/PgCI0Id+KAFHyKpJA0N0h8O4xdJoNyIBAwDZ8LHt0vlnLGwcJFR9X7/
PfWe0PFtC3d7cYY3RopDhnRP7MZs1Wo9nZ4IvlXoEsE2nPkWcns+Wv5Yaewr
s2ra9ZIK7IIJhqKKgmQtCeiXyFwTq+kfunDnxeCavuWL3HuLKIOZf7P9vXXt
XgEir9rCwF8EGAEIABMFAlRJbdIJED62JZ7fId8kAhsMAAD+LAf+KT1EpkwH
0ivTHmYako+6qG6DCtzd3TibWw51cmbY20Ph13NIS/MfBo828S9SXm/sVUzN
/r7qZgZYfI0/j57tG3BguVGm53qya4bINKyi1RjK6aKo/rrzRkh5ZVD5rVNO
E2zzvyYAnLUWG9AV1OYDxcgLrXqEMWlqZAo+Wmg7VrTBmdCGs/BPvscNgQRr
6Gpjgmv9ru6LjRL7vFhEcov/tkBLj+CtaWWFTd1s2vBLOs4rCsD9TT/23vfw
CnokvvVjKYN5oviy61yhpqF1rWlOsxZ4+2sKW3Pq7JLBtmzsZegTONfcQAf7
qqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA==
=2wIY
-----END PGP PRIVATE KEY BLOCK-----
`
) )
var testPrivateKeyRing *crypto.KeyRing
func init() {
privKey, err := crypto.NewKeyFromArmored(testPrivateKey)
if err != nil {
panic(err)
}
privKeyUnlocked, err := privKey.Unlock([]byte(testPrivateKeyPassword))
if err != nil {
panic(err)
}
if testPrivateKeyRing, err = crypto.NewKeyRing(privKeyUnlocked); err != nil {
panic(err)
}
}
type mocksForStore struct { type mocksForStore struct {
tb testing.TB tb testing.TB
@ -48,7 +134,6 @@ type mocksForStore struct {
events *storemocks.MockListener events *storemocks.MockListener
user *storemocks.MockBridgeUser user *storemocks.MockBridgeUser
client *pmapimocks.MockClient client *pmapimocks.MockClient
clientManager *storemocks.MockClientManager
panicHandler *storemocks.MockPanicHandler panicHandler *storemocks.MockPanicHandler
changeNotifier *storemocks.MockChangeNotifier changeNotifier *storemocks.MockChangeNotifier
store *Store store *Store
@ -65,7 +150,6 @@ func initMocks(tb testing.TB) (*mocksForStore, func()) {
events: storemocks.NewMockListener(ctrl), events: storemocks.NewMockListener(ctrl),
user: storemocks.NewMockBridgeUser(ctrl), user: storemocks.NewMockBridgeUser(ctrl),
client: pmapimocks.NewMockClient(ctrl), client: pmapimocks.NewMockClient(ctrl),
clientManager: storemocks.NewMockClientManager(ctrl),
panicHandler: storemocks.NewMockPanicHandler(ctrl), panicHandler: storemocks.NewMockPanicHandler(ctrl),
changeNotifier: storemocks.NewMockChangeNotifier(ctrl), changeNotifier: storemocks.NewMockChangeNotifier(ctrl),
} }
@ -97,30 +181,30 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool, msgs ...*pmapi.M
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.clientManager.EXPECT().GetClient("userID").AnyTimes().Return(mocks.client) mocks.user.EXPECT().GetClient().AnyTimes().Return(mocks.client)
mocks.client.EXPECT().Addresses().Return(pmapi.AddressList{ 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: true},
{ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: pmapi.CanReceive}, {ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: true},
}) })
mocks.client.EXPECT().ListLabels().AnyTimes() mocks.client.EXPECT().ListLabels(gomock.Any()).AnyTimes()
mocks.client.EXPECT().CountMessages("") mocks.client.EXPECT().CountMessages(gomock.Any(), "")
// Call to get latest event ID and then to process first event. // Call to get latest event ID and then to process first event.
eventAfterSyncRequested := make(chan struct{}) eventAfterSyncRequested := make(chan struct{})
mocks.client.EXPECT().GetEvent("").Return(&pmapi.Event{ mocks.client.EXPECT().GetEvent(gomock.Any(), "").Return(&pmapi.Event{
EventID: "firstEventID", EventID: "firstEventID",
}, nil) }, nil)
mocks.client.EXPECT().GetEvent("firstEventID").DoAndReturn(func(_ string) (*pmapi.Event, error) { mocks.client.EXPECT().GetEvent(gomock.Any(), "firstEventID").DoAndReturn(func(_ context.Context, _ string) (*pmapi.Event, error) {
close(eventAfterSyncRequested) close(eventAfterSyncRequested)
return &pmapi.Event{ return &pmapi.Event{
EventID: "latestEventID", EventID: "latestEventID",
}, nil }, nil
}) })
mocks.client.EXPECT().ListMessages(gomock.Any()).Return(msgs, len(msgs), nil).AnyTimes() mocks.client.EXPECT().ListMessages(gomock.Any(), gomock.Any()).Return(msgs, len(msgs), nil).AnyTimes()
for _, msg := range msgs { for _, msg := range msgs {
mocks.client.EXPECT().GetMessage(msg.ID).Return(msg, nil).AnyTimes() mocks.client.EXPECT().GetMessage(gomock.Any(), msg.ID).Return(msg, nil).AnyTimes()
} }
var err error var err error
@ -128,7 +212,6 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool, msgs ...*pmapi.M
nil, // Sentry reporter is not used under unit tests. nil, // Sentry reporter is not used under unit tests.
mocks.panicHandler, mocks.panicHandler,
mocks.user, mocks.user,
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

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"math" "math"
"sync" "sync"
@ -39,10 +40,10 @@ type storeSynchronizer interface {
} }
type messageLister interface { type messageLister interface {
ListMessages(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error) ListMessages(context.Context, *pmapi.MessagesFilter) ([]*pmapi.Message, int, error)
} }
func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api func() messageLister, syncState *syncState) error { func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api 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 +54,7 @@ func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api func()
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 +72,7 @@ func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api func()
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")
@ -147,7 +148,7 @@ func getSplitIDAndCount(labelID string, api messageLister, page int) (string, in
Limit: 1, Limit: 1,
} }
// If the page does not exist, an empty page instead of an error is returned. // If the page does not exist, an empty page instead of an error is returned.
messages, total, err := api.ListMessages(filter) messages, total, err := api.ListMessages(context.Background(), filter)
if err != nil { if err != nil {
return "", 0, errors.Wrap(err, "failed to list messages") return "", 0, errors.Wrap(err, "failed to list messages")
} }
@ -189,7 +190,7 @@ func syncBatch( //nolint[funlen]
log.WithField("begin", filter.BeginID).WithField("end", filter.EndID).Debug("Fetching page") log.WithField("begin", filter.BeginID).WithField("end", filter.EndID).Debug("Fetching page")
messages, _, err := api.ListMessages(filter) messages, _, err := api.ListMessages(context.Background(), filter)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to list messages") return errors.Wrap(err, "failed to list messages")
} }

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"sort" "sort"
"strconv" "strconv"
"sync" "sync"
@ -34,7 +35,7 @@ type mockLister struct {
messageIDs []string messageIDs []string
} }
func (m *mockLister) ListMessages(filter *pmapi.MessagesFilter) (msgs []*pmapi.Message, total int, err error) { func (m *mockLister) ListMessages(_ context.Context, filter *pmapi.MessagesFilter) (msgs []*pmapi.Message, total int, err error) {
if m.err != nil { if m.err != nil {
return nil, 0, m.err return nil, 0, m.err
} }
@ -197,7 +198,7 @@ func TestSyncAllMail(t *testing.T) { //nolint[funlen]
syncState := newSyncState(store, 0, tc.idRanges, tc.idsToBeDeleted) syncState := newSyncState(store, 0, tc.idRanges, tc.idsToBeDeleted)
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState) err := syncAllMail(m.panicHandler, store, api, syncState)
require.Nil(t, err) require.Nil(t, err)
// Check all messages were created or updated. // Check all messages were created or updated.
@ -245,7 +246,7 @@ func TestSyncAllMail_FailedListing(t *testing.T) {
} }
syncState := newTestSyncState(store) syncState := newTestSyncState(store)
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState) err := syncAllMail(m.panicHandler, store, 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")
} }
@ -264,7 +265,7 @@ func TestSyncAllMail_FailedCreateOrUpdateMessage(t *testing.T) {
} }
syncState := newTestSyncState(store) syncState := newTestSyncState(store)
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState) err := syncAllMail(m.panicHandler, store, 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")
} }

View File

@ -17,16 +17,16 @@
package store package store
import "github.com/ProtonMail/proton-bridge/pkg/pmapi" import (
"context"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type PanicHandler interface { type PanicHandler interface {
HandlePanic() HandlePanic()
} }
type ClientManager interface {
GetClient(userID string) pmapi.Client
}
// BridgeUser is subset of bridge.User for use by the Store. // BridgeUser is subset of bridge.User for use by the Store.
type BridgeUser interface { type BridgeUser interface {
ID() string ID() string
@ -35,7 +35,8 @@ type BridgeUser interface {
IsCombinedAddressMode() bool IsCombinedAddressMode() bool
GetPrimaryAddress() string GetPrimaryAddress() string
GetStoreAddresses() []string GetStoreAddresses() []string
UpdateUser() error GetClient() pmapi.Client
UpdateUser(context.Context) error
CloseAllConnections() CloseAllConnections()
CloseConnection(string) CloseConnection(string)
Logout() error Logout() error

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.client().CurrentUser() apiUser, err := store.client().CurrentUser(exposeContextForIMAP())
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 message + all attachments in bytes. // GetMaxUpload returns max size of message + all attachments in bytes.
func (store *Store) GetMaxUpload() (int64, error) { func (store *Store) GetMaxUpload() (int64, error) {
apiUser, err := store.client().CurrentUser() apiUser, err := store.client().CurrentUser(exposeContextForIMAP())
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -147,7 +147,7 @@ func (store *Store) createOrUpdateAddressInfo(addressList pmapi.AddressList) (er
// filterAddresses filters out inactive addresses and ensures the original address is listed first. // filterAddresses filters out inactive addresses and ensures the original address is listed first.
func filterAddresses(addressList pmapi.AddressList) (filteredList pmapi.AddressList) { func filterAddresses(addressList pmapi.AddressList) (filteredList pmapi.AddressList) {
for _, address := range addressList { for _, address := range addressList {
if address.Receive != pmapi.CanReceive { if !address.Receive {
continue continue
} }

View File

@ -38,14 +38,14 @@ func (store *Store) createMailbox(name string) error {
color := store.leastUsedColor() color := store.leastUsedColor()
var exclusive int var exclusive bool
switch { switch {
case strings.HasPrefix(name, UserLabelsPrefix): case strings.HasPrefix(name, UserLabelsPrefix):
name = strings.TrimPrefix(name, UserLabelsPrefix) name = strings.TrimPrefix(name, UserLabelsPrefix)
exclusive = 0 exclusive = false
case strings.HasPrefix(name, UserFoldersPrefix): case strings.HasPrefix(name, UserFoldersPrefix):
name = strings.TrimPrefix(name, UserFoldersPrefix) name = strings.TrimPrefix(name, UserFoldersPrefix)
exclusive = 1 exclusive = true
default: default:
// Ideally we would throw an error here, but then Outlook for // Ideally we would throw an error here, but then Outlook for
// macOS keeps trying to make an IMAP Drafts folder and popping // macOS keeps trying to make an IMAP Drafts folder and popping
@ -55,10 +55,10 @@ func (store *Store) createMailbox(name string) error {
return nil return nil
} }
_, err := store.client().CreateLabel(&pmapi.Label{ _, err := store.client().CreateLabel(exposeContextForIMAP(), &pmapi.Label{
Name: name, Name: name,
Color: color, Color: color,
Exclusive: exclusive, Exclusive: pmapi.Boolean(exclusive),
Type: pmapi.LabelTypeMailbox, Type: pmapi.LabelTypeMailbox,
}) })
return err return err
@ -125,7 +125,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.client().UpdateLabel(&pmapi.Label{ _, err := store.client().UpdateLabel(exposeContextForIMAP(), &pmapi.Label{
ID: labelID, ID: labelID,
Name: newName, Name: newName,
Color: color, Color: color,
@ -142,15 +142,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.client().EmptyFolder(pmapi.SpamLabel, addressID) err = store.client().EmptyFolder(exposeContextForIMAP(), pmapi.SpamLabel, addressID)
case pmapi.TrashLabel: case pmapi.TrashLabel:
err = store.client().EmptyFolder(pmapi.TrashLabel, addressID) err = store.client().EmptyFolder(exposeContextForIMAP(), 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.client().DeleteLabel(labelID) return store.client().DeleteLabel(exposeContextForIMAP(), labelID)
} }
func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error { func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error {
@ -165,7 +165,7 @@ func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) erro
return nil return nil
} }
labels, err := store.client().ListLabels() labels, err := store.client().ListLabels(exposeContextForIMAP())
if err != nil { if err != nil {
return err return err
} }

View File

@ -44,45 +44,132 @@ func (store *Store) CreateDraft(
attachedPublicKey, attachedPublicKey,
attachedPublicKeyName string, attachedPublicKeyName string,
parentID string) (*pmapi.Message, []*pmapi.Attachment, error) { parentID string) (*pmapi.Message, []*pmapi.Attachment, error) {
defer store.eventLoop.pollNow() attachments := store.prepareDraftAttachments(message, attachmentReaders, attachedPublicKey, attachedPublicKeyName)
// Since this is a draft, we don't need to sign it. if err := encryptDraft(kr, message, attachments); err != nil {
if err := message.Encrypt(kr, nil); err != nil {
return nil, nil, errors.Wrap(err, "failed to encrypt draft") return nil, nil, errors.Wrap(err, "failed to encrypt draft")
} }
attachments := message.Attachments if ok, err := store.checkDraftTotalSize(message, attachments); err != nil {
message.Attachments = nil return nil, nil, err
} else if !ok {
return nil, nil, errors.New("message is too large")
}
draftAction := store.getDraftAction(message) draftAction := store.getDraftAction(message)
draft, err := store.client().CreateDraft(message, parentID, draftAction) draft, err := store.client().CreateDraft(exposeContextForSMTP(), 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")
} }
// Do poll only when call to API succeeded.
defer store.eventLoop.pollNow()
createdAttachments := []*pmapi.Attachment{}
for _, att := range attachments {
att.attachment.MessageID = draft.ID
createdAttachment, err := store.client().CreateAttachment(exposeContextForSMTP(), att.attachment, att.encReader, att.sigReader)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create attachment")
}
createdAttachments = append(createdAttachments, createdAttachment)
}
return draft, createdAttachments, nil
}
type draftAttachment struct {
attachment *pmapi.Attachment
reader io.Reader
sigReader io.Reader
encReader io.Reader
}
func (store *Store) prepareDraftAttachments(
message *pmapi.Message,
attachmentReaders []io.Reader,
attachedPublicKey,
attachedPublicKeyName string) []*draftAttachment {
attachments := []*draftAttachment{}
for idx, attachment := range message.Attachments {
attachments = append(attachments, &draftAttachment{
attachment: attachment,
reader: attachmentReaders[idx],
})
}
message.Attachments = nil
if attachedPublicKey != "" { if attachedPublicKey != "" {
attachmentReaders = append(attachmentReaders, strings.NewReader(attachedPublicKey))
publicKeyAttachment := &pmapi.Attachment{ publicKeyAttachment := &pmapi.Attachment{
Name: attachedPublicKeyName + ".asc", Name: attachedPublicKeyName + ".asc",
MIMEType: "application/pgp-keys", MIMEType: "application/pgp-keys",
Header: textproto.MIMEHeader{}, Header: textproto.MIMEHeader{},
} }
attachments = append(attachments, publicKeyAttachment) attachments = append(attachments, &draftAttachment{
attachment: publicKeyAttachment,
reader: strings.NewReader(attachedPublicKey),
})
} }
for idx, attachment := range attachments { return attachments
attachment.MessageID = draft.ID }
attachmentBody, _ := ioutil.ReadAll(attachmentReaders[idx])
createdAttachment, err := store.createAttachment(kr, attachment, attachmentBody) func encryptDraft(kr *crypto.KeyRing, message *pmapi.Message, attachments []*draftAttachment) error {
// Since this is a draft, we don't need to sign it.
if err := message.Encrypt(kr, nil); err != nil {
return errors.Wrap(err, "failed to encrypt message")
}
for _, att := range attachments {
attachment := att.attachment
attachmentBody, err := ioutil.ReadAll(att.reader)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "failed to create attachment for draft") return errors.Wrap(err, "failed to read attachment")
} }
attachments[idx] = createdAttachment r := bytes.NewReader(attachmentBody)
sigReader, err := attachment.DetachedSign(kr, r)
if err != nil {
return errors.Wrap(err, "failed to sign attachment")
}
att.sigReader = sigReader
r = bytes.NewReader(attachmentBody)
encReader, err := attachment.Encrypt(kr, r)
if err != nil {
return errors.Wrap(err, "failed to encrypt attachment")
}
att.encReader = encReader
att.reader = nil
}
return nil
}
func (store *Store) checkDraftTotalSize(message *pmapi.Message, attachments []*draftAttachment) (bool, error) {
maxUpload, err := store.GetMaxUpload()
if err != nil {
return false, err
} }
return draft, attachments, nil msgSize := message.Size
if msgSize == 0 {
msgSize = int64(len(message.Body))
}
var attSize int64
for _, att := range attachments {
b, err := ioutil.ReadAll(att.encReader)
if err != nil {
return false, err
}
attSize += int64(len(b))
att.encReader = bytes.NewBuffer(b)
}
return msgSize+attSize <= maxUpload, nil
} }
func (store *Store) getDraftAction(message *pmapi.Message) int { func (store *Store) getDraftAction(message *pmapi.Message) int {
@ -93,31 +180,10 @@ func (store *Store) getDraftAction(message *pmapi.Message) int {
return pmapi.DraftActionReply return pmapi.DraftActionReply
} }
func (store *Store) createAttachment(kr *crypto.KeyRing, attachment *pmapi.Attachment, attachmentBody []byte) (*pmapi.Attachment, error) {
r := bytes.NewReader(attachmentBody)
sigReader, err := attachment.DetachedSign(kr, r)
if err != nil {
return nil, errors.Wrap(err, "failed to sign attachment")
}
r = bytes.NewReader(attachmentBody)
encReader, err := attachment.Encrypt(kr, r)
if err != nil {
return nil, errors.Wrap(err, "failed to encrypt attachment")
}
createdAttachment, err := store.client().CreateAttachment(attachment, encReader, sigReader)
if err != nil {
return nil, errors.Wrap(err, "failed to create attachment")
}
return createdAttachment, nil
}
// 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.client().SendMessage(messageID, req) _, _, err := store.client().SendMessage(exposeContextForSMTP(), messageID, req)
return err return err
} }

View File

@ -18,10 +18,13 @@
package store package store
import ( import (
"io"
"net/mail" "net/mail"
"strings"
"testing" "testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock"
a "github.com/stretchr/testify/assert" a "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -32,10 +35,10 @@ func TestGetAllMessageIDs(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel})
insertMessage(t, m, "msg3", "Test message 3", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg3", "Test message 3", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg4", "Test message 4", addrID1, 0, []string{}) insertMessage(t, m, "msg4", "Test message 4", addrID1, false, []string{})
checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"}) checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"})
} }
@ -45,7 +48,7 @@ func TestGetMessageFromDB(t *testing.T) {
defer clear() defer clear()
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
tests := []struct{ msgID, wantErr string }{ tests := []struct{ msgID, wantErr string }{
{"msg1", ""}, {"msg1", ""},
@ -70,7 +73,7 @@ func TestCreateOrUpdateMessageMetadata(t *testing.T) {
defer clear() defer clear()
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
msg, err := m.store.getMessageFromDB("msg1") msg, err := m.store.getMessageFromDB("msg1")
require.Nil(t, err) require.Nil(t, err)
@ -102,7 +105,7 @@ func TestCreateOrUpdateMessageMetadata(t *testing.T) {
a.Equal(t, wantHeader, msg.Header) a.Equal(t, wantHeader, msg.Header)
// Check calculated data are not overridden by reinsert. // Check calculated data are not overridden by reinsert.
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
msg, err = m.store.getMessageFromDB("msg1") msg, err = m.store.getMessageFromDB("msg1")
require.Nil(t, err) require.Nil(t, err)
@ -116,8 +119,8 @@ func TestDeleteMessage(t *testing.T) {
defer clear() defer clear()
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel})
require.Nil(t, m.store.deleteMessageEvent("msg1")) require.Nil(t, m.store.deleteMessageEvent("msg1"))
@ -125,17 +128,17 @@ func TestDeleteMessage(t *testing.T) {
checkMailboxMessageIDs(t, m, pmapi.AllMailLabel, []wantID{{"msg2", 2}}) checkMailboxMessageIDs(t, m, pmapi.AllMailLabel, []wantID{{"msg2", 2}})
} }
func insertMessage(t *testing.T, m *mocksForStore, id, subject, sender string, unread int, labelIDs []string) { //nolint[unparam] func insertMessage(t *testing.T, m *mocksForStore, id, subject, sender string, unread bool, labelIDs []string) { //nolint[unparam]
msg := getTestMessage(id, subject, sender, unread, labelIDs) msg := getTestMessage(id, subject, sender, unread, labelIDs)
require.Nil(t, m.store.createOrUpdateMessageEvent(msg)) require.Nil(t, m.store.createOrUpdateMessageEvent(msg))
} }
func getTestMessage(id, subject, sender string, unread int, labelIDs []string) *pmapi.Message { func getTestMessage(id, subject, sender string, unread bool, labelIDs []string) *pmapi.Message {
address := &mail.Address{Address: sender} address := &mail.Address{Address: sender}
return &pmapi.Message{ return &pmapi.Message{
ID: id, ID: id,
Subject: subject, Subject: subject,
Unread: unread, Unread: pmapi.Boolean(unread),
Sender: address, Sender: address,
ToList: []*mail.Address{address}, ToList: []*mail.Address{address},
LabelIDs: labelIDs, LabelIDs: labelIDs,
@ -154,3 +157,47 @@ func checkAllMessageIDs(t *testing.T, m *mocksForStore, wantIDs []string) {
require.Nil(t, allErr) require.Nil(t, allErr)
require.Equal(t, wantIDs, allIds) require.Equal(t, wantIDs, allIds)
} }
func TestCreateDraftCheckMessageSize(t *testing.T) {
m, clear := initMocks(t)
defer clear()
m.newStoreNoEvents(false)
m.client.EXPECT().CurrentUser(gomock.Any()).Return(&pmapi.User{
MaxUpload: 100, // Decrypted message 5 chars, encrypted 500+.
}, nil)
// Even small body is bloated to at least about 500 chars of basic pgp message.
message := &pmapi.Message{
Body: strings.Repeat("a", 5),
}
attachmentReaders := []io.Reader{}
_, _, err := m.store.CreateDraft(testPrivateKeyRing, message, attachmentReaders, "", "", "")
require.EqualError(t, err, "message is too large")
}
func TestCreateDraftCheckMessageWithAttachmentSize(t *testing.T) {
m, clear := initMocks(t)
defer clear()
m.newStoreNoEvents(false)
m.client.EXPECT().CurrentUser(gomock.Any()).Return(&pmapi.User{
MaxUpload: 800, // Decrypted message 5 chars + 5 chars of attachment, encrypted 500+ + 300+.
}, nil)
// Even small body is bloated to at least about 500 chars of basic pgp message.
message := &pmapi.Message{
Body: strings.Repeat("a", 5),
Attachments: []*pmapi.Attachment{
{Name: "name"},
},
}
// Even small attachment is bloated to about 300 chars of encrypted text.
attachmentReaders := []io.Reader{
strings.NewReader(strings.Repeat("b", 5)),
}
_, _, err := m.store.CreateDraft(testPrivateKeyRing, message, attachmentReaders, "", "", "")
require.EqualError(t, err, "message is too large")
}

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strconv"
@ -34,7 +35,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.client().CountMessages("") counts, err := store.client().CountMessages(context.Background(), "")
if err != nil { if err != nil {
return errors.Wrap(err, "cannot update counts from server") return errors.Wrap(err, "cannot update counts from server")
} }
@ -152,7 +153,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, func() messageLister { return store.client() }, syncState) err := syncAllMail(store.panicHandler, store, 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

@ -31,8 +31,8 @@ func TestLoadSaveSyncState(t *testing.T) {
defer clear() defer clear()
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
// Clear everything. // Clear everything.

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/transfer (interfaces: PanicHandler,ClientManager,IMAPClientProvider) // Source: github.com/ProtonMail/proton-bridge/internal/transfer (interfaces: PanicHandler,IMAPClientProvider)
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
@ -7,122 +7,70 @@ package mocks
import ( import (
reflect "reflect" reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imap "github.com/emersion/go-imap" imap "github.com/emersion/go-imap"
sasl "github.com/emersion/go-sasl" sasl "github.com/emersion/go-sasl"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )
// MockPanicHandler is a mock of PanicHandler interface // MockPanicHandler is a mock of PanicHandler interface.
type MockPanicHandler struct { type MockPanicHandler struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder recorder *MockPanicHandlerMockRecorder
} }
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler // MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
type MockPanicHandlerMockRecorder struct { type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler mock *MockPanicHandler
} }
// NewMockPanicHandler creates a new mock instance // NewMockPanicHandler creates a new mock instance.
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler { func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl} mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock} mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder { func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder return m.recorder
} }
// HandlePanic mocks base method // HandlePanic mocks base method.
func (m *MockPanicHandler) HandlePanic() { func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic") m.ctrl.Call(m, "HandlePanic")
} }
// HandlePanic indicates an expected call of HandlePanic // HandlePanic indicates an expected call of HandlePanic.
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call { func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
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 // MockIMAPClientProvider is a mock of IMAPClientProvider 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
}
// 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))
}
// 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)
}
// MockIMAPClientProvider is a mock of IMAPClientProvider interface
type MockIMAPClientProvider struct { type MockIMAPClientProvider struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockIMAPClientProviderMockRecorder recorder *MockIMAPClientProviderMockRecorder
} }
// MockIMAPClientProviderMockRecorder is the mock recorder for MockIMAPClientProvider // MockIMAPClientProviderMockRecorder is the mock recorder for MockIMAPClientProvider.
type MockIMAPClientProviderMockRecorder struct { type MockIMAPClientProviderMockRecorder struct {
mock *MockIMAPClientProvider mock *MockIMAPClientProvider
} }
// NewMockIMAPClientProvider creates a new mock instance // NewMockIMAPClientProvider creates a new mock instance.
func NewMockIMAPClientProvider(ctrl *gomock.Controller) *MockIMAPClientProvider { func NewMockIMAPClientProvider(ctrl *gomock.Controller) *MockIMAPClientProvider {
mock := &MockIMAPClientProvider{ctrl: ctrl} mock := &MockIMAPClientProvider{ctrl: ctrl}
mock.recorder = &MockIMAPClientProviderMockRecorder{mock} mock.recorder = &MockIMAPClientProviderMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIMAPClientProvider) EXPECT() *MockIMAPClientProviderMockRecorder { func (m *MockIMAPClientProvider) EXPECT() *MockIMAPClientProviderMockRecorder {
return m.recorder return m.recorder
} }
// Authenticate mocks base method // Authenticate mocks base method.
func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error { func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Authenticate", arg0) ret := m.ctrl.Call(m, "Authenticate", arg0)
@ -130,13 +78,13 @@ func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error {
return ret0 return ret0
} }
// Authenticate indicates an expected call of Authenticate // Authenticate indicates an expected call of Authenticate.
func (mr *MockIMAPClientProviderMockRecorder) Authenticate(arg0 interface{}) *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) Authenticate(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockIMAPClientProvider)(nil).Authenticate), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockIMAPClientProvider)(nil).Authenticate), arg0)
} }
// Capability mocks base method // Capability mocks base method.
func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) { func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Capability") ret := m.ctrl.Call(m, "Capability")
@ -145,13 +93,13 @@ func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) {
return ret0, ret1 return ret0, ret1
} }
// Capability indicates an expected call of Capability // Capability indicates an expected call of Capability.
func (mr *MockIMAPClientProviderMockRecorder) Capability() *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) Capability() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capability", reflect.TypeOf((*MockIMAPClientProvider)(nil).Capability)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capability", reflect.TypeOf((*MockIMAPClientProvider)(nil).Capability))
} }
// Fetch mocks base method // Fetch mocks base method.
func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error { func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2) ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2)
@ -159,13 +107,13 @@ func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem,
return ret0 return ret0
} }
// Fetch indicates an expected call of Fetch // Fetch indicates an expected call of Fetch.
func (mr *MockIMAPClientProviderMockRecorder) Fetch(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) Fetch(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).Fetch), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).Fetch), arg0, arg1, arg2)
} }
// List mocks base method // List mocks base method.
func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.MailboxInfo) error { func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.MailboxInfo) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", arg0, arg1, arg2) ret := m.ctrl.Call(m, "List", arg0, arg1, arg2)
@ -173,13 +121,13 @@ func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.Mailbox
return ret0 return ret0
} }
// List indicates an expected call of List // List indicates an expected call of List.
func (mr *MockIMAPClientProviderMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIMAPClientProvider)(nil).List), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIMAPClientProvider)(nil).List), arg0, arg1, arg2)
} }
// Login mocks base method // Login mocks base method.
func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error { func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Login", arg0, arg1) ret := m.ctrl.Call(m, "Login", arg0, arg1)
@ -187,13 +135,13 @@ func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error {
return ret0 return ret0
} }
// Login indicates an expected call of Login // Login indicates an expected call of Login.
func (mr *MockIMAPClientProviderMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockIMAPClientProvider)(nil).Login), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockIMAPClientProvider)(nil).Login), arg0, arg1)
} }
// Select mocks base method // Select mocks base method.
func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxStatus, error) { func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxStatus, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Select", arg0, arg1) ret := m.ctrl.Call(m, "Select", arg0, arg1)
@ -202,13 +150,13 @@ func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxSt
return ret0, ret1 return ret0, ret1
} }
// Select indicates an expected call of Select // Select indicates an expected call of Select.
func (mr *MockIMAPClientProviderMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockIMAPClientProvider)(nil).Select), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockIMAPClientProvider)(nil).Select), arg0, arg1)
} }
// State mocks base method // State mocks base method.
func (m *MockIMAPClientProvider) State() imap.ConnState { func (m *MockIMAPClientProvider) State() imap.ConnState {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "State") ret := m.ctrl.Call(m, "State")
@ -216,13 +164,13 @@ func (m *MockIMAPClientProvider) State() imap.ConnState {
return ret0 return ret0
} }
// State indicates an expected call of State // State indicates an expected call of State.
func (mr *MockIMAPClientProviderMockRecorder) State() *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) State() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockIMAPClientProvider)(nil).State)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockIMAPClientProvider)(nil).State))
} }
// Support mocks base method // Support mocks base method.
func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) { func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Support", arg0) ret := m.ctrl.Call(m, "Support", arg0)
@ -231,13 +179,13 @@ func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) {
return ret0, ret1 return ret0, ret1
} }
// Support indicates an expected call of Support // Support indicates an expected call of Support.
func (mr *MockIMAPClientProviderMockRecorder) Support(arg0 interface{}) *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) Support(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Support", reflect.TypeOf((*MockIMAPClientProvider)(nil).Support), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Support", reflect.TypeOf((*MockIMAPClientProvider)(nil).Support), arg0)
} }
// SupportAuth mocks base method // SupportAuth mocks base method.
func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) { func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SupportAuth", arg0) ret := m.ctrl.Call(m, "SupportAuth", arg0)
@ -246,13 +194,13 @@ func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) {
return ret0, ret1 return ret0, ret1
} }
// SupportAuth indicates an expected call of SupportAuth // SupportAuth indicates an expected call of SupportAuth.
func (mr *MockIMAPClientProviderMockRecorder) SupportAuth(arg0 interface{}) *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) SupportAuth(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportAuth", reflect.TypeOf((*MockIMAPClientProvider)(nil).SupportAuth), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportAuth", reflect.TypeOf((*MockIMAPClientProvider)(nil).SupportAuth), arg0)
} }
// UidFetch mocks base method // UidFetch mocks base method.
func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error { func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UidFetch", arg0, arg1, arg2) ret := m.ctrl.Call(m, "UidFetch", arg0, arg1, arg2)
@ -260,7 +208,7 @@ func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchIt
return ret0 return ret0
} }
// UidFetch indicates an expected call of UidFetch // UidFetch indicates an expected call of UidFetch.
func (mr *MockIMAPClientProviderMockRecorder) UidFetch(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockIMAPClientProviderMockRecorder) UidFetch(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UidFetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).UidFetch), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UidFetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).UidFetch), arg0, arg1, arg2)

View File

@ -19,13 +19,14 @@ package transfer
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"net" "net"
"net/http"
"strings" "strings"
"time" "time"
imapID "github.com/ProtonMail/go-imap-id" imapID "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
imapClient "github.com/emersion/go-imap/client" imapClient "github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
@ -38,6 +39,8 @@ const (
imapRetries = 10 imapRetries = 10
imapReconnectTimeout = 30 * time.Minute imapReconnectTimeout = 30 * time.Minute
imapReconnectSleep = time.Minute imapReconnectSleep = time.Minute
protonStatusURL = "http://protonstatus.com/vpn_status"
) )
type imapErrorLogger struct { type imapErrorLogger struct {
@ -118,7 +121,7 @@ func (p *IMAPProvider) tryReconnect(ensureSelectedIn string) error {
return previousErr return previousErr
} }
err := pmapi.CheckConnection() err := checkConnection()
log.WithError(err).Debug("Connection check") log.WithError(err).Debug("Connection check")
if err != nil { if err != nil {
time.Sleep(imapReconnectSleep) time.Sleep(imapReconnectSleep)
@ -286,3 +289,23 @@ func (p *IMAPProvider) fetchHelper(uid bool, ensureSelectedIn string, seqSet *im
return err return err
}, ensureSelectedIn) }, ensureSelectedIn)
} }
// checkConnection returns an error if there is no internet connection.
// Note we don't want to use client manager because it only reports connection
// issues with API; we are only interested here whether we can reach
// third-party IMAP servers.
func checkConnection() error {
client := &http.Client{Timeout: time.Second * 10}
resp, err := client.Get(protonStatusURL)
if err != nil {
return err
}
_ = resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("HTTP status code %d", resp.StatusCode)
}
return nil
}

View File

@ -18,6 +18,7 @@
package transfer package transfer
import ( import (
"context"
"sort" "sort"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
@ -34,25 +35,27 @@ const (
// PMAPIProvider implements import and export to/from ProtonMail server. // PMAPIProvider implements import and export to/from ProtonMail server.
type PMAPIProvider struct { type PMAPIProvider struct {
clientManager ClientManager client pmapi.Client
userID string userID string
addressID string addressID string
keyRing *crypto.KeyRing keyRing *crypto.KeyRing
builder *message.Builder builder *message.Builder
nextImportRequests map[string]*pmapi.ImportMsgReq // Key is msg transfer ID. nextImportRequests map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
nextImportRequestsSize int nextImportRequestsSize int
timeIt *timeIt timeIt *timeIt
connection bool
} }
// NewPMAPIProvider returns new PMAPIProvider. // NewPMAPIProvider returns new PMAPIProvider.
func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*PMAPIProvider, error) { func NewPMAPIProvider(client pmapi.Client, userID, addressID string) (*PMAPIProvider, error) {
provider := &PMAPIProvider{ provider := &PMAPIProvider{
clientManager: clientManager, client: client,
userID: userID, userID: userID,
addressID: addressID, addressID: addressID,
builder: message.NewBuilder(fetchWorkers, attachWorkers, buildWorkers), builder: message.NewBuilder(fetchWorkers, attachWorkers, buildWorkers),
nextImportRequests: map[string]*pmapi.ImportMsgReq{}, nextImportRequests: map[string]*pmapi.ImportMsgReq{},
nextImportRequestsSize: 0, nextImportRequestsSize: 0,
@ -61,7 +64,7 @@ func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*P
} }
if addressID != "" { if addressID != "" {
keyRing, err := clientManager.GetClient(userID).KeyRingForAddressID(addressID) keyRing, err := client.KeyRingForAddressID(addressID)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get key ring") return nil, errors.Wrap(err, "failed to get key ring")
} }
@ -71,10 +74,6 @@ func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*P
return provider, nil return provider, nil
} }
func (p *PMAPIProvider) client() pmapi.Client {
return p.clientManager.GetClient(p.userID)
}
// ID returns identifier of current setup of PMAPI provider. // ID returns identifier of current setup of PMAPI provider.
// Identification is unique per user. // Identification is unique per user.
func (p *PMAPIProvider) ID() string { func (p *PMAPIProvider) ID() string {
@ -83,7 +82,7 @@ func (p *PMAPIProvider) ID() string {
// Mailboxes returns all available labels in ProtonMail account. // Mailboxes returns all available labels in ProtonMail account.
func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) { func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) {
labels, err := p.client().ListLabels() labels, err := p.client.ListLabels(context.Background())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -92,7 +91,7 @@ func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox,
emptyLabelsMap := map[string]bool{} emptyLabelsMap := map[string]bool{}
if !includeEmpty { if !includeEmpty {
messagesCounts, err := p.client().CountMessages(p.addressID) messagesCounts, err := p.client.CountMessages(context.Background(), p.addressID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -120,7 +119,7 @@ func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox,
ID: label.ID, ID: label.ID,
Name: label.Name, Name: label.Name,
Color: label.Color, Color: label.Color,
IsExclusive: label.Exclusive == 1, IsExclusive: bool(label.Exclusive),
}) })
} }
return mailboxes, nil return mailboxes, nil
@ -160,10 +159,10 @@ func (l byFoldersLabels) Swap(i, j int) {
// Less sorts first folders, then labels, by user order. // Less sorts first folders, then labels, by user order.
func (l byFoldersLabels) Less(i, j int) bool { func (l byFoldersLabels) Less(i, j int) bool {
if l[i].Exclusive == 1 && l[j].Exclusive == 0 { if l[i].Exclusive && !l[j].Exclusive {
return true return true
} }
if l[i].Exclusive == 0 && l[j].Exclusive == 1 { if !l[i].Exclusive && l[j].Exclusive {
return false return false
} }
return l[i].Order < l[j].Order return l[i].Order < l[j].Order

View File

@ -157,7 +157,7 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
body, err := p.builder.NewJobWithOptions( body, err := p.builder.NewJobWithOptions(
context.Background(), context.Background(),
p.client(), p.client,
msg.ID, msg.ID,
message.JobOptions{IgnoreDecryptionErrors: !skipEncryptedMessages}, message.JobOptions{IgnoreDecryptionErrors: !skipEncryptedMessages},
).GetResult() ).GetResult()
@ -169,14 +169,9 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
return Message{Body: []byte(msg.Body)}, err return Message{Body: []byte(msg.Body)}, err
} }
unread := false
if msg.Unread == 1 {
unread = true
}
return Message{ return Message{
ID: msgID, ID: msgID,
Unread: unread, Unread: bool(msg.Unread),
Body: body, Body: body,
Sources: []Mailbox{rule.SourceMailbox}, Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes, Targets: rule.TargetMailboxes,

View File

@ -19,6 +19,7 @@ package transfer
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -51,15 +52,10 @@ func (p *PMAPIProvider) CreateMailbox(mailbox Mailbox) (Mailbox, error) {
return Mailbox{}, errors.New("mailbox is already created") return Mailbox{}, errors.New("mailbox is already created")
} }
exclusive := 0 label, err := p.client.CreateLabel(context.Background(), &pmapi.Label{
if mailbox.IsExclusive {
exclusive = 1
}
label, err := p.client().CreateLabel(&pmapi.Label{
Name: mailbox.Name, Name: mailbox.Name,
Color: mailbox.Color, Color: mailbox.Color,
Exclusive: exclusive, Exclusive: pmapi.Boolean(mailbox.IsExclusive),
Type: pmapi.LabelTypeMailbox, Type: pmapi.LabelTypeMailbox,
}) })
if err != nil { if err != nil {
@ -118,12 +114,20 @@ func (p *PMAPIProvider) transferDraft(rules transferRules, progress *Progress, m
progress.messageImported(msg.ID, importedID, err) progress.messageImported(msg.ID, importedID, err)
} }
func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string, error) { func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string, error) { //nolint[funlen]
message, attachmentReaders, err := p.parseMessage(msg) message, attachmentReaders, err := p.parseMessage(msg)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to parse message") return "", errors.Wrap(err, "failed to parse message")
} }
if message.Sender == nil {
mainAddress := p.client.Addresses().Main()
message.Sender = &mail.Address{
Name: mainAddress.DisplayName,
Address: mainAddress.Email,
}
}
// Trying to encrypt an encrypted draft will return an error; // Trying to encrypt an encrypted draft will return an error;
// users are forbidden to import messages encrypted with foreign keys to drafts. // users are forbidden to import messages encrypted with foreign keys to drafts.
if message.IsEncrypted() { if message.IsEncrypted() {
@ -186,7 +190,7 @@ func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress,
return return
} }
importMsgReqSize := len(importMsgReq.Body) importMsgReqSize := len(importMsgReq.Message)
if p.nextImportRequestsSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.nextImportRequests) == pmapiImportBatchMaxItems { if p.nextImportRequestsSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.nextImportRequests) == pmapiImportBatchMaxItems {
preparedImportRequestsCh <- p.nextImportRequests preparedImportRequestsCh <- p.nextImportRequests
p.nextImportRequests = map[string]*pmapi.ImportMsgReq{} p.nextImportRequests = map[string]*pmapi.ImportMsgReq{}
@ -218,11 +222,6 @@ func (p *PMAPIProvider) generateImportMsgReq(rules transferRules, progress *Prog
} }
} }
unread := 0
if msg.Unread {
unread = 1
}
labelIDs := []string{} labelIDs := []string{}
for _, target := range msg.Targets { for _, target := range msg.Targets {
// Frontend should not set All Mail to Rules, but to be sure... // Frontend should not set All Mail to Rules, but to be sure...
@ -235,12 +234,14 @@ func (p *PMAPIProvider) generateImportMsgReq(rules transferRules, progress *Prog
} }
return &pmapi.ImportMsgReq{ return &pmapi.ImportMsgReq{
AddressID: p.addressID, Metadata: &pmapi.ImportMetadata{
Body: body, AddressID: p.addressID,
Unread: unread, Unread: pmapi.Boolean(msg.Unread),
Time: message.Time, Time: message.Time,
Flags: computeMessageFlags(message.Header), Flags: computeMessageFlags(message.Header),
LabelIDs: labelIDs, LabelIDs: labelIDs,
},
Message: body,
}, nil }, nil
} }
@ -285,7 +286,7 @@ func (p *PMAPIProvider) importMessages(progress *Progress, importRequests map[st
} }
importMsgIDs := []string{} importMsgIDs := []string{}
importMsgRequests := []*pmapi.ImportMsgReq{} importMsgRequests := pmapi.ImportMsgReqs{}
for msgID, req := range importRequests { for msgID, req := range importRequests {
importMsgIDs = append(importMsgIDs, msgID) importMsgIDs = append(importMsgIDs, msgID)
importMsgRequests = append(importMsgRequests, req) importMsgRequests = append(importMsgRequests, req)
@ -319,7 +320,7 @@ func (p *PMAPIProvider) importMessages(progress *Progress, importRequests map[st
func (p *PMAPIProvider) importMessage(msgSourceID string, progress *Progress, req *pmapi.ImportMsgReq) (importedID string, importedErr error) { func (p *PMAPIProvider) importMessage(msgSourceID string, progress *Progress, req *pmapi.ImportMsgReq) (importedID string, importedErr error) {
progress.callWrap(func() error { progress.callWrap(func() error {
results, err := p.importRequest(msgSourceID, []*pmapi.ImportMsgReq{req}) results, err := p.importRequest(msgSourceID, pmapi.ImportMsgReqs{req})
if err != nil { if err != nil {
return errors.Wrap(err, "failed to import messages") return errors.Wrap(err, "failed to import messages")
} }

View File

@ -19,6 +19,7 @@ package transfer
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"testing" "testing"
"time" "time"
@ -33,7 +34,7 @@ func TestPMAPIProviderMailboxes(t *testing.T) {
defer m.ctrl.Finish() defer m.ctrl.Finish()
setupPMAPIClientExpectationForExport(&m) setupPMAPIClientExpectationForExport(&m)
provider, err := NewPMAPIProvider(m.clientManager, "user", "addressID") provider, err := NewPMAPIProvider(m.pmapiClient, "user", "addressID")
r.NoError(t, err) r.NoError(t, err)
tests := []struct { tests := []struct {
@ -78,7 +79,7 @@ func TestPMAPIProviderTransferTo(t *testing.T) {
defer m.ctrl.Finish() defer m.ctrl.Finish()
setupPMAPIClientExpectationForExport(&m) setupPMAPIClientExpectationForExport(&m)
provider, err := NewPMAPIProvider(m.clientManager, "user", "addressID") provider, err := NewPMAPIProvider(m.pmapiClient, "user", "addressID")
r.NoError(t, err) r.NoError(t, err)
rules, rulesClose := newTestRules(t) rules, rulesClose := newTestRules(t)
@ -96,7 +97,7 @@ func TestPMAPIProviderTransferFrom(t *testing.T) {
defer m.ctrl.Finish() defer m.ctrl.Finish()
setupPMAPIClientExpectationForImport(&m) setupPMAPIClientExpectationForImport(&m)
provider, err := NewPMAPIProvider(m.clientManager, "user", "addressID") provider, err := NewPMAPIProvider(m.pmapiClient, "user", "addressID")
r.NoError(t, err) r.NoError(t, err)
rules, rulesClose := newTestRules(t) rules, rulesClose := newTestRules(t)
@ -114,7 +115,7 @@ func TestPMAPIProviderTransferFromDraft(t *testing.T) {
defer m.ctrl.Finish() defer m.ctrl.Finish()
setupPMAPIClientExpectationForImportDraft(&m) setupPMAPIClientExpectationForImportDraft(&m)
provider, err := NewPMAPIProvider(m.clientManager, "user", "addressID") provider, err := NewPMAPIProvider(m.pmapiClient, "user", "addressID")
r.NoError(t, err) r.NoError(t, err)
rules, rulesClose := newTestRules(t) rules, rulesClose := newTestRules(t)
@ -133,9 +134,9 @@ func TestPMAPIProviderTransferFromTo(t *testing.T) {
setupPMAPIClientExpectationForExport(&m) setupPMAPIClientExpectationForExport(&m)
setupPMAPIClientExpectationForImport(&m) setupPMAPIClientExpectationForImport(&m)
source, err := NewPMAPIProvider(m.clientManager, "user", "addressID") source, err := NewPMAPIProvider(m.pmapiClient, "user", "addressID")
r.NoError(t, err) r.NoError(t, err)
target, err := NewPMAPIProvider(m.clientManager, "user", "addressID") target, err := NewPMAPIProvider(m.pmapiClient, "user", "addressID")
r.NoError(t, err) r.NoError(t, err)
rules, rulesClose := newTestRules(t) rules, rulesClose := newTestRules(t)
@ -151,22 +152,22 @@ func setupPMAPIRules(rules transferRules) {
func setupPMAPIClientExpectationForExport(m *mocks) { func setupPMAPIClientExpectationForExport(m *mocks) {
m.pmapiClient.EXPECT().KeyRingForAddressID(gomock.Any()).Return(m.keyring, nil).AnyTimes() m.pmapiClient.EXPECT().KeyRingForAddressID(gomock.Any()).Return(m.keyring, nil).AnyTimes()
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{ m.pmapiClient.EXPECT().ListLabels(gomock.Any()).Return([]*pmapi.Label{
{ID: "label1", Name: "Foo", Color: "blue", Exclusive: 0, Order: 2}, {ID: "label1", Name: "Foo", Color: "blue", Exclusive: false, Order: 2},
{ID: "label2", Name: "Bar", Color: "green", Exclusive: 0, Order: 1}, {ID: "label2", Name: "Bar", Color: "green", Exclusive: false, Order: 1},
{ID: "folder1", Name: "One", Color: "red", Exclusive: 1, Order: 1}, {ID: "folder1", Name: "One", Color: "red", Exclusive: true, Order: 1},
{ID: "folder2", Name: "Two", Color: "orange", Exclusive: 1, Order: 2}, {ID: "folder2", Name: "Two", Color: "orange", Exclusive: true, Order: 2},
}, nil).AnyTimes() }, nil).AnyTimes()
m.pmapiClient.EXPECT().CountMessages(gomock.Any()).Return([]*pmapi.MessagesCount{ m.pmapiClient.EXPECT().CountMessages(gomock.Any(), gomock.Any()).Return([]*pmapi.MessagesCount{
{LabelID: "label1", Total: 10}, {LabelID: "label1", Total: 10},
{LabelID: "label2", Total: 0}, {LabelID: "label2", Total: 0},
{LabelID: "folder1", Total: 20}, {LabelID: "folder1", Total: 20},
}, nil).AnyTimes() }, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{ m.pmapiClient.EXPECT().ListMessages(gomock.Any(), gomock.Any()).Return([]*pmapi.Message{
{ID: "msg1"}, {ID: "msg1"},
{ID: "msg2"}, {ID: "msg2"},
}, 2, nil).AnyTimes() }, 2, nil).AnyTimes()
m.pmapiClient.EXPECT().GetMessage(gomock.Any()).DoAndReturn(func(msgID string) (*pmapi.Message, error) { m.pmapiClient.EXPECT().GetMessage(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, msgID string) (*pmapi.Message, error) {
return &pmapi.Message{ return &pmapi.Message{
ID: msgID, ID: msgID,
Body: string(getTestMsgBody(msgID)), Body: string(getTestMsgBody(msgID)),
@ -177,11 +178,11 @@ func setupPMAPIClientExpectationForExport(m *mocks) {
func setupPMAPIClientExpectationForImport(m *mocks) { func setupPMAPIClientExpectationForImport(m *mocks) {
m.pmapiClient.EXPECT().KeyRingForAddressID(gomock.Any()).Return(m.keyring, nil).AnyTimes() m.pmapiClient.EXPECT().KeyRingForAddressID(gomock.Any()).Return(m.keyring, nil).AnyTimes()
m.pmapiClient.EXPECT().Import(gomock.Any()).DoAndReturn(func(requests []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) { m.pmapiClient.EXPECT().Import(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, requests pmapi.ImportMsgReqs) ([]*pmapi.ImportMsgRes, error) {
results := []*pmapi.ImportMsgRes{} results := []*pmapi.ImportMsgRes{}
for _, request := range requests { for _, request := range requests {
for _, msgID := range []string{"msg1", "msg2"} { for _, msgID := range []string{"msg1", "msg2"} {
if bytes.Contains(request.Body, []byte(msgID)) { if bytes.Contains(request.Message, []byte(msgID)) {
results = append(results, &pmapi.ImportMsgRes{MessageID: msgID, Error: nil}) results = append(results, &pmapi.ImportMsgRes{MessageID: msgID, Error: nil})
} }
} }
@ -192,7 +193,7 @@ func setupPMAPIClientExpectationForImport(m *mocks) {
func setupPMAPIClientExpectationForImportDraft(m *mocks) { func setupPMAPIClientExpectationForImportDraft(m *mocks) {
m.pmapiClient.EXPECT().KeyRingForAddressID(gomock.Any()).Return(m.keyring, nil).AnyTimes() m.pmapiClient.EXPECT().KeyRingForAddressID(gomock.Any()).Return(m.keyring, nil).AnyTimes()
m.pmapiClient.EXPECT().CreateDraft(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(msg *pmapi.Message, parentID string, action int) (*pmapi.Message, error) { m.pmapiClient.EXPECT().CreateDraft(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, msg *pmapi.Message, parentID string, action int) (*pmapi.Message, error) {
r.Equal(m.t, msg.Subject, "draft1") r.Equal(m.t, msg.Subject, "draft1")
msg.ID = "draft1" msg.ID = "draft1"
return msg, nil return msg, nil

View File

@ -18,6 +18,7 @@
package transfer package transfer
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"time" "time"
@ -29,9 +30,17 @@ import (
const ( const (
pmapiRetries = 10 pmapiRetries = 10
pmapiReconnectTimeout = 30 * time.Minute pmapiReconnectTimeout = 30 * time.Minute
pmapiReconnectSleep = time.Minute pmapiReconnectSleep = 10 * time.Second
) )
func (p *PMAPIProvider) SetConnectionUp() {
p.connection = true
}
func (p *PMAPIProvider) SetConnectionDown() {
p.connection = false
}
func (p *PMAPIProvider) ensureConnection(callback func() error) error { func (p *PMAPIProvider) ensureConnection(callback func() error) error {
var callErr error var callErr error
for i := 1; i <= pmapiRetries; i++ { for i := 1; i <= pmapiRetries; i++ {
@ -57,11 +66,8 @@ func (p *PMAPIProvider) tryReconnect() error {
return previousErr return previousErr
} }
err := p.clientManager.CheckConnection() if !p.connection {
log.WithError(err).Debug("Connection check")
if err != nil {
time.Sleep(pmapiReconnectSleep) time.Sleep(pmapiReconnectSleep)
previousErr = err
continue continue
} }
@ -77,7 +83,7 @@ func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*
p.timeIt.start("listing", key) p.timeIt.start("listing", key)
defer p.timeIt.stop("listing", key) defer p.timeIt.stop("listing", key)
messages, count, err = p.client().ListMessages(filter) messages, count, err = p.client.ListMessages(context.Background(), filter)
return err return err
}) })
return return
@ -88,18 +94,18 @@ func (p *PMAPIProvider) getMessage(msgID string) (message *pmapi.Message, err er
p.timeIt.start("download", msgID) p.timeIt.start("download", msgID)
defer p.timeIt.stop("download", msgID) defer p.timeIt.stop("download", msgID)
message, err = p.client().GetMessage(msgID) message, err = p.client.GetMessage(context.Background(), msgID)
return err return err
}) })
return return
} }
func (p *PMAPIProvider) importRequest(msgSourceID string, req []*pmapi.ImportMsgReq) (res []*pmapi.ImportMsgRes, err error) { func (p *PMAPIProvider) importRequest(msgSourceID string, req pmapi.ImportMsgReqs) (res []*pmapi.ImportMsgRes, err error) {
err = p.ensureConnection(func() error { err = p.ensureConnection(func() error {
p.timeIt.start("upload", msgSourceID) p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID) defer p.timeIt.stop("upload", msgSourceID)
res, err = p.client().Import(req) res, err = p.client.Import(context.Background(), req)
return err return err
}) })
return return
@ -110,7 +116,7 @@ func (p *PMAPIProvider) createDraft(msgSourceID string, message *pmapi.Message,
p.timeIt.start("upload", msgSourceID) p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID) defer p.timeIt.stop("upload", msgSourceID)
draft, err = p.client().CreateDraft(message, parent, action) draft, err = p.client.CreateDraft(context.Background(), message, parent, action)
return err return err
}) })
return return
@ -123,7 +129,7 @@ func (p *PMAPIProvider) createAttachment(msgSourceID string, att *pmapi.Attachme
p.timeIt.start("upload", key) p.timeIt.start("upload", key)
defer p.timeIt.stop("upload", key) defer p.timeIt.stop("upload", key)
created, err = p.client().CreateAttachment(att, r, sig) created, err = p.client.CreateAttachment(context.Background(), att, r, sig)
return err return err
}) })
return return

View File

@ -23,7 +23,6 @@ import (
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
transfermocks "github.com/ProtonMail/proton-bridge/internal/transfer/mocks" transfermocks "github.com/ProtonMail/proton-bridge/internal/transfer/mocks"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks" pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )
@ -33,10 +32,8 @@ type mocks struct {
ctrl *gomock.Controller ctrl *gomock.Controller
panicHandler *transfermocks.MockPanicHandler panicHandler *transfermocks.MockPanicHandler
clientManager *transfermocks.MockClientManager
imapClientProvider *transfermocks.MockIMAPClientProvider imapClientProvider *transfermocks.MockIMAPClientProvider
pmapiClient *pmapimocks.MockClient pmapiClient *pmapimocks.MockClient
pmapiConfig *pmapi.ClientConfig
keyring *crypto.KeyRing keyring *crypto.KeyRing
} }
@ -49,15 +46,11 @@ func initMocks(t *testing.T) mocks {
ctrl: mockCtrl, ctrl: mockCtrl,
panicHandler: transfermocks.NewMockPanicHandler(mockCtrl), panicHandler: transfermocks.NewMockPanicHandler(mockCtrl),
clientManager: transfermocks.NewMockClientManager(mockCtrl),
imapClientProvider: transfermocks.NewMockIMAPClientProvider(mockCtrl), imapClientProvider: transfermocks.NewMockIMAPClientProvider(mockCtrl),
pmapiClient: pmapimocks.NewMockClient(mockCtrl), pmapiClient: pmapimocks.NewMockClient(mockCtrl),
pmapiConfig: &pmapi.ClientConfig{},
keyring: newTestKeyring(), keyring: newTestKeyring(),
} }
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).AnyTimes()
return m return m
} }

View File

@ -17,10 +17,6 @@
package transfer package transfer
import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type PanicHandler interface { type PanicHandler interface {
HandlePanic() HandlePanic()
} }
@ -32,8 +28,3 @@ type MetricsManager interface {
Cancel() Cancel()
Fail() Fail()
} }
type ClientManager interface {
GetClient(userID string) pmapi.Client
CheckConnection() error
}

View File

@ -18,6 +18,7 @@
package updater package updater
import ( import (
"bytes"
"encoding/json" "encoding/json"
"io" "io"
@ -31,10 +32,6 @@ import (
var ErrManualUpdateRequired = errors.New("manual update is required") var ErrManualUpdateRequired = errors.New("manual update is required")
type ClientProvider interface {
GetAnonymousClient() pmapi.Client
}
type Installer interface { type Installer interface {
InstallUpdate(*semver.Version, io.Reader) error InstallUpdate(*semver.Version, io.Reader) error
} }
@ -46,7 +43,7 @@ type Settings interface {
} }
type Updater struct { type Updater struct {
cm ClientProvider cm pmapi.Manager
installer Installer installer Installer
settings Settings settings Settings
kr *crypto.KeyRing kr *crypto.KeyRing
@ -59,7 +56,7 @@ type Updater struct {
} }
func New( func New(
cm ClientProvider, cm pmapi.Manager,
installer Installer, installer Installer,
s Settings, s Settings,
kr *crypto.KeyRing, kr *crypto.KeyRing,
@ -87,13 +84,10 @@ func New(
func (u *Updater) Check() (VersionInfo, error) { func (u *Updater) Check() (VersionInfo, error) {
logrus.Info("Checking for updates") logrus.Info("Checking for updates")
client := u.cm.GetAnonymousClient() b, err := u.cm.DownloadAndVerify(
defer client.Logout() u.kr,
r, err := client.DownloadAndVerify(
u.getVersionFileURL(), u.getVersionFileURL(),
u.getVersionFileURL()+".sig", u.getVersionFileURL()+".sig",
u.kr,
) )
if err != nil { if err != nil {
return VersionInfo{}, err return VersionInfo{}, err
@ -101,7 +95,7 @@ func (u *Updater) Check() (VersionInfo, error) {
var versionMap VersionMap var versionMap VersionMap
if err := json.NewDecoder(r).Decode(&versionMap); err != nil { if err := json.Unmarshal(b, &versionMap); err != nil {
return VersionInfo{}, err return VersionInfo{}, err
} }
@ -141,15 +135,12 @@ func (u *Updater) InstallUpdate(update VersionInfo) error {
return u.locker.doOnce(func() error { return u.locker.doOnce(func() error {
logrus.WithField("package", update.Package).Info("Installing update package") logrus.WithField("package", update.Package).Info("Installing update package")
client := u.cm.GetAnonymousClient() b, err := u.cm.DownloadAndVerify(u.kr, update.Package, update.Package+".sig")
defer client.Logout()
r, err := client.DownloadAndVerify(update.Package, update.Package+".sig", u.kr)
if err != nil { if err != nil {
return errors.Wrap(ErrDownloadVerify, err.Error()) return errors.Wrap(ErrDownloadVerify, err.Error())
} }
if err := u.installer.InstallUpdate(update.Version, r); err != nil { if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {
return errors.Wrap(ErrInstall, err.Error()) return errors.Wrap(ErrInstall, err.Error())
} }

View File

@ -18,7 +18,6 @@
package updater package updater
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
@ -40,9 +39,9 @@ func TestCheck(t *testing.T) {
c := gomock.NewController(t) c := gomock.NewController(t)
defer c.Finish() defer c.Finish()
client := mocks.NewMockClient(c) cm := mocks.NewMockManager(c)
updater := newTestUpdater(client, "1.1.0", false) updater := newTestUpdater(cm, "1.1.0", false)
versionMap := VersionMap{ versionMap := VersionMap{
"stable": VersionInfo{ "stable": VersionInfo{
@ -53,13 +52,11 @@ func TestCheck(t *testing.T) {
}, },
} }
client.EXPECT().DownloadAndVerify( cm.EXPECT().DownloadAndVerify(
gomock.Any(),
updater.getVersionFileURL(), updater.getVersionFileURL(),
updater.getVersionFileURL()+".sig", updater.getVersionFileURL()+".sig",
gomock.Any(), ).Return(mustMarshal(t, versionMap), nil)
).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil)
client.EXPECT().Logout()
version, err := updater.Check() version, err := updater.Check()
@ -71,9 +68,9 @@ func TestCheckEarlyAccess(t *testing.T) {
c := gomock.NewController(t) c := gomock.NewController(t)
defer c.Finish() defer c.Finish()
client := mocks.NewMockClient(c) cm := mocks.NewMockManager(c)
updater := newTestUpdater(client, "1.1.0", true) updater := newTestUpdater(cm, "1.1.0", true)
versionMap := VersionMap{ versionMap := VersionMap{
"stable": VersionInfo{ "stable": VersionInfo{
@ -90,13 +87,11 @@ func TestCheckEarlyAccess(t *testing.T) {
}, },
} }
client.EXPECT().DownloadAndVerify( cm.EXPECT().DownloadAndVerify(
gomock.Any(),
updater.getVersionFileURL(), updater.getVersionFileURL(),
updater.getVersionFileURL()+".sig", updater.getVersionFileURL()+".sig",
gomock.Any(), ).Return(mustMarshal(t, versionMap), nil)
).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil)
client.EXPECT().Logout()
version, err := updater.Check() version, err := updater.Check()
@ -108,18 +103,16 @@ func TestCheckBadSignature(t *testing.T) {
c := gomock.NewController(t) c := gomock.NewController(t)
defer c.Finish() defer c.Finish()
client := mocks.NewMockClient(c) cm := mocks.NewMockManager(c)
updater := newTestUpdater(client, "1.2.0", false) updater := newTestUpdater(cm, "1.2.0", false)
client.EXPECT().DownloadAndVerify( cm.EXPECT().DownloadAndVerify(
gomock.Any(),
updater.getVersionFileURL(), updater.getVersionFileURL(),
updater.getVersionFileURL()+".sig", updater.getVersionFileURL()+".sig",
gomock.Any(),
).Return(nil, errors.New("bad signature")) ).Return(nil, errors.New("bad signature"))
client.EXPECT().Logout()
_, err := updater.Check() _, err := updater.Check()
assert.Error(t, err) assert.Error(t, err)
@ -129,9 +122,9 @@ func TestIsUpdateApplicable(t *testing.T) {
c := gomock.NewController(t) c := gomock.NewController(t)
defer c.Finish() defer c.Finish()
client := mocks.NewMockClient(c) cm := mocks.NewMockManager(c)
updater := newTestUpdater(client, "1.4.0", false) updater := newTestUpdater(cm, "1.4.0", false)
versionOld := VersionInfo{ versionOld := VersionInfo{
Version: semver.MustParse("1.3.0"), Version: semver.MustParse("1.3.0"),
@ -165,9 +158,9 @@ func TestCanInstall(t *testing.T) {
c := gomock.NewController(t) c := gomock.NewController(t)
defer c.Finish() defer c.Finish()
client := mocks.NewMockClient(c) cm := mocks.NewMockManager(c)
updater := newTestUpdater(client, "1.4.0", false) updater := newTestUpdater(cm, "1.4.0", false)
versionManual := VersionInfo{ versionManual := VersionInfo{
Version: semver.MustParse("1.5.0"), Version: semver.MustParse("1.5.0"),
@ -192,9 +185,9 @@ func TestInstallUpdate(t *testing.T) {
c := gomock.NewController(t) c := gomock.NewController(t)
defer c.Finish() defer c.Finish()
client := mocks.NewMockClient(c) cm := mocks.NewMockManager(c)
updater := newTestUpdater(client, "1.4.0", false) updater := newTestUpdater(cm, "1.4.0", false)
latestVersion := VersionInfo{ latestVersion := VersionInfo{
Version: semver.MustParse("1.5.0"), Version: semver.MustParse("1.5.0"),
@ -203,13 +196,11 @@ func TestInstallUpdate(t *testing.T) {
RolloutProportion: 1.0, RolloutProportion: 1.0,
} }
client.EXPECT().DownloadAndVerify( cm.EXPECT().DownloadAndVerify(
gomock.Any(),
latestVersion.Package, latestVersion.Package,
latestVersion.Package+".sig", latestVersion.Package+".sig",
gomock.Any(), ).Return([]byte("tgz_data_here"), nil)
).Return(bytes.NewReader([]byte("tgz_data_here")), nil)
client.EXPECT().Logout()
err := updater.InstallUpdate(latestVersion) err := updater.InstallUpdate(latestVersion)
@ -220,9 +211,9 @@ func TestInstallUpdateBadSignature(t *testing.T) {
c := gomock.NewController(t) c := gomock.NewController(t)
defer c.Finish() defer c.Finish()
client := mocks.NewMockClient(c) cm := mocks.NewMockManager(c)
updater := newTestUpdater(client, "1.4.0", false) updater := newTestUpdater(cm, "1.4.0", false)
latestVersion := VersionInfo{ latestVersion := VersionInfo{
Version: semver.MustParse("1.5.0"), Version: semver.MustParse("1.5.0"),
@ -231,14 +222,12 @@ func TestInstallUpdateBadSignature(t *testing.T) {
RolloutProportion: 1.0, RolloutProportion: 1.0,
} }
client.EXPECT().DownloadAndVerify( cm.EXPECT().DownloadAndVerify(
gomock.Any(),
latestVersion.Package, latestVersion.Package,
latestVersion.Package+".sig", latestVersion.Package+".sig",
gomock.Any(),
).Return(nil, errors.New("bad signature")) ).Return(nil, errors.New("bad signature"))
client.EXPECT().Logout()
err := updater.InstallUpdate(latestVersion) err := updater.InstallUpdate(latestVersion)
assert.Error(t, err) assert.Error(t, err)
@ -248,9 +237,9 @@ func TestInstallUpdateAlreadyOngoing(t *testing.T) {
c := gomock.NewController(t) c := gomock.NewController(t)
defer c.Finish() defer c.Finish()
client := mocks.NewMockClient(c) cm := mocks.NewMockManager(c)
updater := newTestUpdater(client, "1.4.0", false) updater := newTestUpdater(cm, "1.4.0", false)
updater.installer = &fakeInstaller{delay: 2 * time.Second} updater.installer = &fakeInstaller{delay: 2 * time.Second}
@ -261,13 +250,11 @@ func TestInstallUpdateAlreadyOngoing(t *testing.T) {
RolloutProportion: 1.0, RolloutProportion: 1.0,
} }
client.EXPECT().DownloadAndVerify( cm.EXPECT().DownloadAndVerify(
gomock.Any(),
latestVersion.Package, latestVersion.Package,
latestVersion.Package+".sig", latestVersion.Package+".sig",
gomock.Any(), ).Return([]byte("tgz_data_here"), nil)
).Return(bytes.NewReader([]byte("tgz_data_here")), nil)
client.EXPECT().Logout()
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
@ -288,9 +275,9 @@ func TestInstallUpdateAlreadyOngoing(t *testing.T) {
wg.Wait() wg.Wait()
} }
func newTestUpdater(client *mocks.MockClient, curVer string, earlyAccess bool) *Updater { func newTestUpdater(manager pmapi.Manager, curVer string, earlyAccess bool) *Updater {
return New( return New(
&fakeClientProvider{client: client}, manager,
&fakeInstaller{}, &fakeInstaller{},
newFakeSettings(0.5, earlyAccess), newFakeSettings(0.5, earlyAccess),
nil, nil,
@ -299,14 +286,6 @@ func newTestUpdater(client *mocks.MockClient, curVer string, earlyAccess bool) *
) )
} }
type fakeClientProvider struct {
client *mocks.MockClient
}
func (p *fakeClientProvider) GetAnonymousClient() pmapi.Client {
return p.client
}
type fakeInstaller struct { type fakeInstaller struct {
bad bool bad bool
delay time.Duration delay time.Duration

View File

@ -47,8 +47,8 @@ type Credentials struct {
UserID, // Do not marshal; used as a key. UserID, // Do not marshal; used as a key.
Name, Name,
Emails, Emails,
APIToken, APIToken string
MailboxPassword, MailboxPassword []byte
BridgePassword, BridgePassword,
Version string Version string
Timestamp int64 Timestamp int64
@ -58,15 +58,15 @@ type Credentials struct {
func (s *Credentials) Marshal() string { func (s *Credentials) Marshal() string {
items := []string{ items := []string{
s.Name, // 0 s.Name, // 0
s.Emails, // 1 s.Emails, // 1
s.APIToken, // 2 s.APIToken, // 2
s.MailboxPassword, // 3 string(s.MailboxPassword), // 3
s.BridgePassword, // 4 s.BridgePassword, // 4
s.Version, // 5 s.Version, // 5
"", // 6 "", // 6
"", // 7 "", // 7
"", // 8 "", // 8
} }
items[6] = fmt.Sprint(s.Timestamp) items[6] = fmt.Sprint(s.Timestamp)
@ -97,7 +97,7 @@ func (s *Credentials) Unmarshal(secret string) error {
s.Name = items[0] s.Name = items[0]
s.Emails = items[1] s.Emails = items[1]
s.APIToken = items[2] s.APIToken = items[2]
s.MailboxPassword = items[3] s.MailboxPassword = []byte(items[3])
switch len(items) { switch len(items) {
case itemLengthBridge: case itemLengthBridge:
@ -143,9 +143,24 @@ func (s *Credentials) CheckPassword(password string) error {
func (s *Credentials) Logout() { func (s *Credentials) Logout() {
s.APIToken = "" s.APIToken = ""
s.MailboxPassword = ""
for i := range s.MailboxPassword {
s.MailboxPassword[i] = 0
}
s.MailboxPassword = []byte{}
} }
func (s *Credentials) IsConnected() bool { func (s *Credentials) IsConnected() bool {
return s.APIToken != "" && s.MailboxPassword != "" return s.APIToken != "" && len(s.MailboxPassword) != 0
}
func (s *Credentials) SplitAPIToken() (string, string, error) {
split := strings.Split(s.APIToken, ":")
if len(split) != 2 {
return "", "", errors.New("malformed API token")
}
return split[0], split[1], nil
} }

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