Compare commits

...

51 Commits

Author SHA1 Message Date
9d576beeb8 Bridge 1.5.4 Golden Gate 2020-12-14 21:15:25 +01:00
e3332d1cb6 Windows needs txt suffix 2020-12-14 16:04:24 +00:00
f59f68f894 Fix Windows license path 2020-12-14 16:04:24 +00:00
9c881a02d6 Fix license path for arch 2020-12-14 16:04:24 +00:00
7b21c2d734 Log warning about permanently deleting messages 2020-12-14 09:21:04 +01:00
9fdc5960bf ci: use large runners for integration tests 2020-12-10 12:08:23 +00:00
fe853efe32 clear unreleased 2020-12-10 13:08:13 +01:00
9b82c03959 Import-Export app Elbe 1.2.3
• Allow an import of already encrypted messages (as cypher text)
    • Cosmetic GUI changes
    • Better error handling
    • Installation issues on linux
2020-12-09 17:57:44 +01:00
914d1b27b5 Bridge Golden-Gate 1.5.3
• Support read confirmations
• Adding GPLv3 licence button to the GUI
• Improved testing

• AppleMail crashes (timestamp related)
• Encoding errors
• Installation issues on linux
2020-12-09 17:56:21 +01:00
f295d03641 License button to open LICENSE file 2020-12-09 15:58:41 +00:00
8515f6e6ac Switch to bridge-internal:latest runner 2020-12-09 15:33:29 +00:00
4d330e24c1 Add qt docs target to system-qt builds 2020-12-09 15:33:29 +00:00
a7a52bc57e testing native qt builds with CI 2020-12-09 15:33:29 +00:00
3cef7985d3 rename COPYING.md file 2020-12-09 10:14:41 +00:00
40db822450 Replace old date to not crash Apple Mail 2020-12-09 07:22:04 +00:00
2de202ca02 fix: set flags in status response 2020-12-08 09:06:22 +00:00
38eb9fdac7 feat(GODT-906): support rfc2047-encoded content transfer encodings 2020-12-07 13:03:49 +01:00
f469d34781 Send unilateral responses before sending OK 2020-12-04 12:37:02 +00:00
33dfc5ce09 Use function to determine which functions to skip 2020-12-02 12:31:18 +00:00
2100e2ff7c Enhanced sentry reporting 2020-12-02 12:31:18 +00:00
e9b7cce138 chore: bump go-rfc5322 dependency to v0.2.1 2020-12-02 10:58:56 +01:00
6877a5a15d Add changelog linter 2020-12-01 08:48:21 +00:00
64206e69bd Fix of all known flaky tests 2020-11-30 16:15:53 +01:00
7643c76cb1 Merge branch 'release/elbe' into devel 2020-11-30 11:59:41 +01:00
b0f59273d3 ci: beefier runners for heavier jobs 2020-11-30 08:15:09 +01:00
af8eb9d37d Adding documentation about therecipe/qt 2020-11-27 08:58:05 +00:00
635e51f32f Upgrade to latest go-smtp 2020-11-27 09:23:18 +01:00
ca962ce5ad Import encrypted messages as is 2020-11-27 09:09:11 +01:00
a50266cdc0 Merge branch 'master' into release/elbe 2020-11-27 07:46:40 +01:00
6230200218 Import-Export app Elbe 1.2.2
Changed
* Improvements to the import from large mbox files with multiple labels
* Not allow to run multiple instances of the app or transfers at the same time
* Better handling and displaying of skipped messages
* Various enhancements of the import process related to parsing
* Cosmetic GUI changes
* Better error handling

Fixed
* Linux font issues - Fedora specific
* App response to the user pausing and canceling import or export
* Upgrade errors
2020-11-27 07:37:08 +01:00
f96cd167ef Merge branch 'release/golden-gate' into devel 2020-11-26 09:32:06 +01:00
072ce54fe1 Bridge 1.5.2 Golden Gate
Release Notes
* Improved package creation logic
* Refactor of sending functions to simplify code maintenance
* Added tests for package creation

Fixed
* Bridge crashes related to labels handling
* GUI popup related to TLS connection error
* An issue where a random session key is included in the data payload
* Error handling (including improved detection)
2020-11-24 10:38:36 +01:00
d043cb9086 test: disable flaky expunge tests (followup GODT-881) 2020-11-23 14:41:47 +00:00
1f31df3a94 Bridge 1.5.1 Golden Gate
Release Notes
* Improved package creation logic
* Refactor of sending functions to simplify code maintenance
* Added tests for package creation

Fixed
* Bridge crashes related to labels handling
* GUI popup related to TLS connection error
* An issue where a random session key is included in the data payload
* Error handling (including improved detection)
2020-11-23 07:43:43 +01:00
9ee30e4923 Add sentry fingerprint 2020-11-20 14:44:42 +00:00
7b44f12ab1 Update sentry client 2020-11-20 14:44:42 +00:00
874882b554 Logic change to follow old code. 2020-11-20 13:39:13 +00:00
945bdf4c60 Custom types for flags and encrypted outside test 2020-11-20 13:39:13 +00:00
6e1e5a2afe re-organise test definitions 2020-11-20 13:39:13 +00:00
b709b51790 Simplify test cases 2020-11-20 13:39:13 +00:00
d380485bb6 Fixing lint and integration tests, changelog, GODT-880, and typos 2020-11-20 13:39:13 +00:00
87c8228cd0 rename 2020-11-20 13:39:13 +00:00
152046bf97 refactor smtp sending
* [x] move package creation logic to `pmapi.SendMessageReq`
* [ ] write test of package creation logic
    * [x] internal
    * [x] plain
    * [x] external encrypted
    * [ ] signature ???
    * [x] attachments
2020-11-20 13:39:13 +00:00
a0fbed5859 use unreleased for changes 2020-11-20 14:35:24 +01:00
89e9e17d26 Fix typos in InlineLabelSelect.qml 2020-11-20 07:43:43 +00:00
b595247392 chore: add version info to github issue template 2020-11-19 16:57:21 +01:00
9d50a8cef2 Add OS to app version 2020-11-18 09:46:01 +00:00
f888176485 Build creates proper binary names 2020-11-18 08:56:38 +00:00
2f9876ad74 Remove unnecessary semicolon 2020-11-13 13:18:16 +00:00
53404122cc Integration test of sending and manual appending to Sent mailbox 2020-11-13 13:18:16 +00:00
ba65494fce Try load messages one-by-one 2020-11-13 09:43:04 +00:00
119 changed files with 3492 additions and 1160 deletions

2
.gitattributes vendored
View File

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

View File

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

View File

@ -1,4 +1,4 @@
image: gitlab.protontech.ch:4567/go/bridge-internal image: gitlab.protontech.ch:4567/go/bridge-internal:latest
before_script: before_script:
- eval $(ssh-agent -s) - eval $(ssh-agent -s)
@ -45,6 +45,8 @@ lint:
- branches - branches
script: script:
- make lint - make lint
tags:
- medium
test: test:
stage: test stage: test
@ -60,6 +62,8 @@ test:
- pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'` - pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'`
# Then finally run the tests # Then finally run the tests
- make test - make test
tags:
- medium
test-integration: test-integration:
stage: test stage: test
@ -67,6 +71,8 @@ test-integration:
- branches - branches
script: script:
- VERBOSITY=debug make -C test test - VERBOSITY=debug make -C test test
tags:
- large
dependency-updates: dependency-updates:
stage: test stage: test
@ -85,6 +91,8 @@ dependency-updates:
# 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 regardless of the expiry time.
# Introduced in GitLab 13.0 behind a disabled feature flag, and made the default behavior in GitLab 13.4. # 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:
- large
build-linux: build-linux:
extends: .build-base extends: .build-base

View File

@ -13,7 +13,16 @@ To enable the sending of crash reports using Sentry please set the
Otherwise, the sending of crash reports will be disabled. Otherwise, the sending of crash reports will be disabled.
## Build ## Build
* for Windows please unset the `MSYSTEM` variable In order to build Bridge or Import-Export app with Qt interface we are using
[Qt Go Binding](https://github.com/therecipe/qt). The dependencies and
installation of this tool is part of `make build` target. If you have issues
with installation of therecipe/qt we recommend to follow [this
wiki](https://github.com/therecipe/qt/wiki/Installation-on-Linux)
Please note that `$(go env GOPATH)/bin` must be in your `PATH` to ensure
binaries installed by `therecipe/qt` (such as `qtdeploy`) are found. Also,
before you start build **on Windows**, please unset the `MSYSTEM` variable
```bash ```bash
export MSYSTEM= export MSYSTEM=

View File

@ -2,12 +2,66 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [IE 1.2.1] Elbe ## [Bridge 1.5.4] Golden Gate
### Added
* Log warning about permanently deleting messages.
### Fixed
* License path on Arch and Windows.
## [Bridge 1.5.3] Golden Gate [Import-Export 1.2.3] Elbe
### Added
* GODT-906 Handle RFC2047-encoded content transfer encoding values.
* GODT-887 Make supports build with native Qt.
### Changed
* GODT-893 Bump go-rfc5322 dependency to v0.2.1 to properly detect syntax errors during parsing.
* GODT-892 Swap type and value from sentry exception and cut panic handlers from the traceback.
* GODT-854 EXPUNGE and FETCH unilateral responses are returned before OK EXPUNGE or OK STORE, respectively.
* #109 Renamed COPYING.md to not be read by [pkg-go-dev](https://pkg.go.dev/license-policy).
### Removed
* GODT-651 Build creates proper binary names.
* GODT-148 Allow import (using the Import-Export app) of already encrypted messages as is.
* GODT-202 Update to latest go-smtp.
### Fixed
* GODT-135 Support parameters in SMTP `FROM MAIL` command, such as `BODY=7BIT`, or empty value `FROM MAIL:<>` used by some clients.
* GODT-338 GODT-781 GODT-857 GODT-866 Flaky tests.
* GODT-773 Replace old dates with birthday of RFC822 to not crash Apple Mail. Original is available under `X-Original-Date` header.
## [Bridge 1.5.2] Golden Gate
### Changed
* GODT-883 Use `ClearPacket` for `text/plain` with signature.
## [Bridge 1.5.1] Golden Gate
### Added
* GODT-701 Try load messages one-by-one if IMAP server errors with batch load
and not interrupt the transfer.
* GODT-878 Tests for send packet creation logic.
### Changed
* GODT-180 Updated Sentry client.
* GODT-651 Build creates proper binary names.
* GODT-878 Fix an issue where the random session key is inadvertently sent to
the Proton server. The data payload is always encrypted within TLS, but this
is still a potential privacy problem. Discovered by Proton's internal
security audit team.
* GODT-878 Refactor and move the send packet creation logic to `pmapi.SendMessageReq`.
* GODT-878 Encryption of session keys moved to pmapi.
## [IE 1.2.1, 1.2.2] Elbe
### Added ### Added
* GODT-799 Skipped messages do not change total counts but shows as separate number. * GODT-799 Skipped messages do not change total counts but shows as separate number.
## Fixed ### Fixed
* GODT-799 Fix skipping unwanted folders importing from mbox files. * GODT-799 Fix skipping unwanted folders importing from mbox files.
* GODT-769 Close connection before deleting labels to prevent panics accessing deleted bucket. * GODT-769 Close connection before deleting labels to prevent panics accessing deleted bucket.
@ -16,6 +70,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.5.0] Golden Gate ## [Bridge 1.5.0] Golden Gate
### Changed ### Changed
* Updated go-mbox dependency back to upstream. * Updated go-mbox dependency back to upstream.
@ -23,7 +78,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-847 Waiting for unilateral update during deleting the message. * GODT-847 Waiting for unilateral update during deleting the message.
* GODT-849 Show in error counts in the end also lost messages. * GODT-849 Show in error counts in the end also lost messages.
* GODT-835 Do not include conversation ID in references to show properly conversation threads in clients. * GODT-835 Do not include conversation ID in references to show properly conversation threads in clients.
* GODT-685 Improve deb packaging regarding dejavu font * GODT-685 Improve deb packaging regarding dejavu font.
## [IE 1.2.0] Elbe ## [IE 1.2.0] Elbe
@ -35,7 +90,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Fixed ### Fixed
* GODT-677 Windows IE: global import settings not fit in window. * GODT-677 Windows IE: global import settings not fit in window.
* GODT-794 Congo fails to update to Danube * GODT-794 Congo fails to update to Danube.
* GODT-749 Don't force PGP/Inline when sending plaintext messages. * GODT-749 Don't force PGP/Inline when sending plaintext messages.
* GODT-764 Fix deadlock in integration tests for Import-Export. * GODT-764 Fix deadlock in integration tests for Import-Export.
* GODT-662 Do not resume paused transfer progress after dismissing cancel popup. * GODT-662 Do not resume paused transfer progress after dismissing cancel popup.
@ -63,8 +118,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Fixed ### Fixed
* GODT-798 Replace, don't add, transfer encoding when making body 7-bit clean. * GODT-798 Replace, don't add, transfer encoding when making body 7-bit clean.
* Move/Copy duplicate for emails with References in Outlook * Move/Copy duplicate for emails with References in Outlook.
* CSB-247 Cannot update from 1.4.0 * CSB-247 Cannot update from 1.4.0.
## [Bridge 1.4.3] Forth ## [Bridge 1.4.3] Forth
@ -90,7 +145,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-776 Fix crash when IMAP client connects while account is logging in. * GODT-776 Fix crash when IMAP client connects while account is logging in.
### Changed ### Changed
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8 * Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8.
* GODT-785 Clear separation of different message IDs in integration tests. * GODT-785 Clear separation of different message IDs in integration tests.
### Changed ### Changed
* GODT-741 Import-Export shows "Unable to parse time" notice instead of zero time in error report window. * GODT-741 Import-Export shows "Unable to parse time" notice instead of zero time in error report window.
@ -147,60 +202,60 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-461 Add support for `\Deleted` flag. * GODT-461 Add support for `\Deleted` flag.
### Changed ### Changed
* GODT-462 Pausing event loop while FETCHing to prevent EXPUNGE * GODT-462 Pausing event loop while FETCHing to prevent EXPUNGE.
* Wait for unilateral response to be delivered * Wait for unilateral response to be delivered.
* GODT-409 Set flags have to replace all flags. * GODT-409 Set flags have to replace all flags.
* GODT-531 Better way to add trusted certificate in macOS. * GODT-531 Better way to add trusted certificate in macOS.
* Bumped golangci-lint to v1.29.0 * Bumped golangci-lint to v1.29.0.
* GODT-549 Check log file size more often to prevent huge log files. * GODT-549 Check log file size more often to prevent huge log files.
* Bumped various dependencies: * Bumped various dependencies:
* andybalholm/cascadia v1.1.0 -> v1.2.0 * Updated andybalholm/cascadia v1.1.0 -> v1.2.0.
* emersion/go-imap-specialuse 20161227184202-ba031ced6a62 -> 20200722111535-598ff00e4075 * Updated emersion/go-imap-specialuse 20161227184202-ba031ced6a62 -> 20200722111535-598ff00e4075.
* emersion/go-sasl 20191210011802-430746ea8b9b -> 20200509203442-7bfe0ed36a21 * Updated emersion/go-sasl 20191210011802-430746ea8b9b -> 20200509203442-7bfe0ed36a21.
* github.com/go-resty/resty/v2 v2.2.0 -> v2.3.0 * Updated github.com/go-resty/resty/v2 v2.2.0 -> v2.3.0.
* github.com/golang/mock v1.4.3 -> v1.4.4 * Updated github.com/golang/mock v1.4.3 -> v1.4.4.
* github.com/google/go-cmp v0.4.0 -> v0.5.1 * Updated github.com/google/go-cmp v0.4.0 -> v0.5.1.
* github.com/hashicorp/go-multierror v1.0.0 -> v1.1.0 * Updated github.com/hashicorp/go-multierror v1.0.0 -> v1.1.0.
* github.com/jaytaylor/html2text 20200220170450-61d9dc4d7195 -> 20200412013138-3577fbdbcff7 * Updated github.com/jaytaylor/html2text 20200220170450-61d9dc4d7195 -> 20200412013138-3577fbdbcff7.
* github.com/jhillyerd/enmime v0.8.0 -> v0.8.1 * Updated github.com/jhillyerd/enmime v0.8.0 -> v0.8.1.
* github.com/keybase/go-keychain 20200218013740-86d4642e4ce2 -> 20200502122510-cda31fe0c86d * Updated github.com/keybase/go-keychain 20200218013740-86d4642e4ce2 -> 20200502122510-cda31fe0c86d.
* github.com/logrusorgru/aurora 20200102142835-e9ef32dff381 -> v2.0.3+incompatible * Updated github.com/logrusorgru/aurora 20200102142835-e9ef32dff381 -> v2.0.3+incompatible.
* github.com/miekg/dns v1.1.29 -> v1.1.30 * Updated github.com/miekg/dns v1.1.29 -> v1.1.30.
* github.com/nsf/jsondiff 20190712045011-8443391ee9b6 -> 20200515183724-f29ed568f4ce * Updated github.com/nsf/jsondiff 20190712045011-8443391ee9b6 -> 20200515183724-f29ed568f4ce.
* github.com/sirupsen/logrus v1.4.2 -> v1.6.0 * Updated github.com/sirupsen/logrus v1.4.2 -> v1.6.0.
* github.com/stretchr/testify v1.5.1 -> v1.6.1 * Updated github.com/stretchr/testify v1.5.1 -> v1.6.1.
* github.com/therecipe/qt 20200126204426-5074eb6d8c41 -> 20200701200531-7f61353ee73e * Updated github.com/therecipe/qt 20200126204426-5074eb6d8c41 -> 20200701200531-7f61353ee73e.
* github.com/urfave/cli v1.22.3 -> v1.22.4 * Updated github.com/urfave/cli v1.22.3 -> v1.22.4.
* golang.org/x/net 20200301022130-244492dfa37a -> 20200707034311-ab3426394381 * Updated golang.org/x/net 20200301022130-244492dfa37a -> 20200707034311-ab3426394381.
* golang.org/x/text v0.3.2 -> v0.3.3 * Updated golang.org/x/text v0.3.2 -> v0.3.3.
* Set first-start to false in bridge, not in frontend. * Set first-start to false in bridge, not in frontend.
* GODT-400 Refactor sendingInfo. * GODT-400 Refactor sendingInfo.
* GODT-513 Update routes to API v4. * GODT-513 Update routes to API v4.
* GODT-551 Do not ignore errors during message flagging. * GODT-551 Do not ignore errors during message flagging.
* GODT-380 Adding IE GUI to Bridge repo and building * GODT-380 Adding IE GUI to Bridge repo and building.
* BR: extend functionality of PopupDialog * BR: extend functionality of PopupDialog.
* BR: makefile APP_VERSION instead of BRIDGE_VERSION * BR: makefile APP_VERSION instead of BRIDGE_VERSION.
* BR: use common logs function for Qt * BR: use common logs function for Qt.
* BR: change `go.progressDescription` to `string` * BR: change `go.progressDescription` to `string`.
* IE: Rounded button has fa-icon * IE: Rounded button has fa-icon.
* IE: `Upgrade``Update` * IE: `Upgrade``Update`.
* IE: Moving `AccountModel` to `qt-common` * IE: Moving `AccountModel` to `qt-common`.
* IE: Added `ReportBug` to `internal/importexport` * IE: Added `ReportBug` to `internal/importexport`.
* IE: Added event watch in GUI * IE: Added event watch in GUI.
* IE: Removed `onLoginFinished` * IE: Removed `onLoginFinished`.
* Structure for transfer rules in QML * Structure for transfer rules in QML.
* GODT-213 Convert panics from message parser to error. * GODT-213 Convert panics from message parser to error.
* GODT-585 Do not allow deleting messages from All Mail. * GODT-585 Do not allow deleting messages from All Mail.
### Fixed ### Fixed
* GODT-655 Fix date picker with automatic Windows DST * GODT-655 Fix date picker with automatic Windows DST.
* GODT-454 Fix send on closed channel when receiving unencrypted send confirmation from GUI. * GODT-454 Fix send on closed channel when receiving unencrypted send confirmation from GUI.
* GODT-597 Duplicate sending when draft creation takes too long * GODT-597 Duplicate sending when draft creation takes too long.
* GODT-634 Hover on links in popups. * GODT-634 Hover on links in popups.
## [v1.3.x] Emma (v1.3.2 beta 2020-08-04, v1.3.3 beta 2020-08-06, v1.3.3 live 2020-08-12) ## [Bridge 1.3.x] Emma (v1.3.2 beta 2020-08-04, v1.3.3 beta 2020-08-06, v1.3.3 live 2020-08-12)
### Added ### Added
* GODT-554 Detect and notify about "bad certificate" IMAP TLS error. * GODT-554 Detect and notify about "bad certificate" IMAP TLS error.
@ -260,7 +315,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Issue causing deadlock when reloading users keys due to double-locking of a mutex. * Issue causing deadlock when reloading users keys due to double-locking of a mutex.
* Correctly handle failure to unlock single key. * Correctly handle failure to unlock single key.
* GODT-479 Fix flaky integration tests. * GODT-479 Fix flaky integration tests.
* GODT-484 Fix infinite loop when decoding invalid 2231 charset * GODT-484 Fix infinite loop when decoding invalid 2231 charset.
* GODT-267 Correctly detect if a message is a draft even if does not have DraftLabel. * GODT-267 Correctly detect if a message is a draft even if does not have DraftLabel.
* GODT-308 Reduce minimum read speed threshold to avoid issues with flaky internet. * GODT-308 Reduce minimum read speed threshold to avoid issues with flaky internet.
* GODT-321 Changing address ordering would cause all messages to disappear in combined mode. * GODT-321 Changing address ordering would cause all messages to disappear in combined mode.
@ -269,7 +324,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-427 Fix race condition in auth refresh that could cause user to be logged out. * GODT-427 Fix race condition in auth refresh that could cause user to be logged out.
## [v1.2.8] Donghai-fix-append (beta 2020-06-XXX) ## [Bridge 1.2.8] Donghai-fix-append (beta 2020-06-XXX)
### Changed ### Changed
* GODT-396 reduce number of EXISTS calls. * GODT-396 reduce number of EXISTS calls.
@ -278,7 +333,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Fixed ### Fixed
* GODT-502 Fixed crash when unable to parse a message header. * GODT-502 Fixed crash when unable to parse a message header.
## [v1.2.7] Donghai-fix-sync - (beta 2020-05-07 live 2020-04-20) ## [Bridge 1.2.7] Donghai-fix-sync - (beta 2020-05-07 live 2020-04-20)
### Added ### Added
* IMAP extension MOVE with UIDPLUS support. * IMAP extension MOVE with UIDPLUS support.
@ -301,7 +356,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Use correct binary name when finding location of addcert.scpt. * Use correct binary name when finding location of addcert.scpt.
## [v1.2.6] Donghai - beta (2020-03-31) ## [Bridge 1.2.6] Donghai - beta (2020-03-31)
### Added ### Added
* GODT-145 Support drafts. * GODT-145 Support drafts.
@ -356,13 +411,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* UserIDs were not checked when importing to Sent folder (affects copying from account1/sent to account2/sent). * UserIDs were not checked when importing to Sent folder (affects copying from account1/sent to account2/sent).
## [v1.2.5] Charles - live (2020-03-11) beta (from 2020-02-10) ## [Bridge 1.2.5] Charles - live (2020-03-11) beta (from 2020-02-10)
### Hotfix
* CSB-40 panic in credential store.
* Keyring unlocking locker.
* No panic on failed html parse.
* Too many open files.
### Added ### Added
* GODT-112 Migration of preferences from c10 to c11. * GODT-112 Migration of preferences from c10 to c11.
@ -407,13 +456,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Separated IMAP to store and IMAP. * Separated IMAP to store and IMAP.
* Store is responsible for everything about db and calls to pmapi, including event loop, sync, address mode. * Store is responsible for everything about db and calls to pmapi, including event loop, sync, address mode.
* IMAP is responsible only for IMAP interfaces. * IMAP is responsible only for IMAP interfaces.
* Event loop is only one per ProtonMail account (instead of one per alias) * Event loop is only one per ProtonMail account (instead of one per alias).
* It also means only one database per account (instead of one per address) * It also means only one database per account (instead of one per address).
* Changing address mode is not destroying database, only buckets with IDs mapping (keeping metadata for account) * Changing address mode is not destroying database, only buckets with IDs mapping (keeping metadata for account).
* Before first sync we set event ID so we will not miss changes happening during sync. * Before first sync we set event ID so we will not miss changes happening during sync.
* Thanks to previous point we are not starting new sync when we finish first one because of unprocessed events. * Thanks to previous point we are not starting new sync when we finish first one because of unprocessed events.
* Sync is not blocking event loop (user can get new messages even during sync) * Sync is not blocking event loop (user can get new messages even during sync).
* Sync is not blocking reading operations (user can list mailboxes even before first sync is done) * Sync is not blocking reading operations (user can list mailboxes even before first sync is done).
* Sync is not blocking writing operations such as mark messages read/unread and so on. * Sync is not blocking writing operations such as mark messages read/unread and so on.
* Most operations have to be passed to API and only event loop is writing them to the database. * Most operations have to be passed to API and only event loop is writing them to the database.
* Avoid relying on counts API endpoint; use event counts as much as possible. * Avoid relying on counts API endpoint; use event counts as much as possible.
@ -423,8 +472,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Synchronisation will create a label if not yet present. * Synchronisation will create a label if not yet present.
* Labels and Folders (including system folders) are stored in DB together with their counts for offline read-out. * Labels and Folders (including system folders) are stored in DB together with their counts for offline read-out.
* AddressIDs for all user addresses are stored in DB. * AddressIDs for all user addresses are stored in DB.
* IMAP updates channel is set when an IMAP client connects (and IMAP updates are dropped until then) * IMAP updates channel is set when an IMAP client connects (and IMAP updates are dropped until then).
* DB keeps track of address mode (split/combined) * DB keeps track of address mode (split/combined).
* Event loop starts as soon as user is initialised (i.e. logged in), not just when imap is connected. * Event loop starts as soon as user is initialised (i.e. logged in), not just when imap is connected.
* Use pmapi v1.0.13. * Use pmapi v1.0.13.
* Logout user if initialisation fails. * Logout user if initialisation fails.
@ -432,6 +481,10 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Use godog v0.8.0 under new name 'cucumber' (instead of DATA-DOG). * Use godog v0.8.0 under new name 'cucumber' (instead of DATA-DOG).
### Fixed ### Fixed
* CSB-40 panic in credential store.
* Keyring unlocking locker.
* No panic on failed html parse.
* Too many open files.
* #1057 Logging in to an already logged in user would display unrelated error "invalid mailbox password". * #1057 Logging in to an already logged in user would display unrelated error "invalid mailbox password".
* #1056 Changing mailbox password sometimes didn't log out user. * #1056 Changing mailbox password sometimes didn't log out user.
* #1066 Split address mode can not work when credentials store is cleared. * #1066 Split address mode can not work when credentials store is cleared.
@ -447,7 +500,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-103 User keys were not unlocked later if they were not unlocked during startup. * GODT-103 User keys were not unlocked later if they were not unlocked during startup.
## [v1.2.4] Brooklyn beta (2019-12-16) ## [Bridge 1.2.4] Brooklyn beta (2019-12-16)
### Added ### Added
* #976: fix slow authentication. * #976: fix slow authentication.
@ -462,7 +515,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Fixed an issue where entering an in-use port multiple times via the CLI would make bridge use it. * Fixed an issue where entering an in-use port multiple times via the CLI would make bridge use it.
* Update therecipe/qt and Qt to 5.13. * Update therecipe/qt and Qt to 5.13.
## [v1.2.3] Akashi - live (2019-11-05) beta (2019-10-22) ## [Bridge 1.2.3] Akashi - live (2019-11-05) beta (2019-10-22)
### Added ### Added
* #963 report first-start metric with bridge version. * #963 report first-start metric with bridge version.
@ -492,17 +545,17 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Code made compatible with name changes in go-pmapi. * Code made compatible with name changes in go-pmapi.
## [v1.2.2] - beta and live 2019-09-06 ## [Bridge 1.2.2] - beta and live 2019-09-06
### Changed ### Changed
* User compare case insensitive. * User compare case insensitive.
## [v1.2.1] - beta and live 2019-09-05 ## [Bridge 1.2.1] - beta and live 2019-09-05
### Changed ### Changed
* #924 fix start of bridge without internet connection. * #924 fix start of bridge without internet connection.
## [v1.2.0] - beta 2019-08-22 ## [Bridge 1.2.0] - beta 2019-08-22
### Added ### Added
* #903 added http.Client timeout to not hang out forever. * #903 added http.Client timeout to not hang out forever.
@ -602,7 +655,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Handle logout in event loop. * Handle logout in event loop.
## [v1.1.6] - 2019-07-09 (beta 2019-07-01) ## [Bridge 1.1.6] - 2019-07-09 (beta 2019-07-01)
### Added ### Added
* #841 assume text/plain during sending e-mails when missing content type. * #841 assume text/plain during sending e-mails when missing content type.
@ -634,7 +687,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Lint corrections. * Lint corrections.
## [v1.1.5] - 2019-05-23 (beta 2019-05-23, 2019-05-16) ## [Bridge 1.1.5] - 2019-05-23 (beta 2019-05-23, 2019-05-16)
### Changed ### Changed
* Fix custom message format. * Fix custom message format.
@ -648,7 +701,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Only one crash from second instance. * Only one crash from second instance.
* During event `MessageID` in log as field. * During event `MessageID` in log as field.
## [v1.1.4 live] - 2019-04-10 (beta 2019-04-05, 2019-03-27) ## [Bridge 1.1.4 live] - 2019-04-10 (beta 2019-04-05, 2019-03-27)
### Added ### Added
* Address with port to IMAP debug. * Address with port to IMAP debug.
@ -669,7 +722,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Removed ### Removed
* #750 Synchronization after 450 messages. * #750 Synchronization after 450 messages.
## [v1.1.3] - 2019-03-04 ## [Bridge 1.1.3] - 2019-03-04
### Added ### Added
* Sentry crash reporting in main. * Sentry crash reporting in main.
@ -681,13 +734,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* #720 sync every 3 pages. * #720 sync every 3 pages.
* #512 extending list of charsets go-pm-mime!4. * #512 extending list of charsets go-pm-mime!4.
## [v1.1.2] - beta only 2019-02-21 ## [Bridge 1.1.2] - beta only 2019-02-21
### Changed ### Changed
* #512 fail on unknown charset. * #512 fail on unknown charset.
* #729 #733 visitor for MIME parsing. * #729 #733 visitor for MIME parsing.
## [v1.1.1] - 2019-02-11 ## [Bridge 1.1.1] - 2019-02-11
### Added ### Added
* #671 include `name` param in attachment `Content-Type` (in addition to `Content-Disposition` param `filename`). * #671 include `name` param in attachment `Content-Type` (in addition to `Content-Disposition` param `filename`).
* #671 do not include content headers for section requests e.g. `BODY.PEEK[2]`. * #671 do not include content headers for section requests e.g. `BODY.PEEK[2]`.
@ -740,7 +793,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* SMTP stays authenticated after sent message. * SMTP stays authenticated after sent message.
* Reduce memory, processor and number of API calls. * Reduce memory, processor and number of API calls.
## [v1.1.0] - 2018-10-22 ## [Bridge 1.1.0] - 2018-10-22
### Removed ### Removed
* `go-pmapi.Config.ClientSecret`. * `go-pmapi.Config.ClientSecret`.
@ -816,11 +869,11 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Additional synchronization of mail database. * Additional synchronization of mail database.
## [v1.0.6 silent] - 2018-08-23 ## [Bridge 1.0.6 silent] - 2018-08-23
### Added ### Added
* New svg icon in linux package. * New svg icon in linux package.
## [v1.0.6] - 2018-08-09 ## [Bridge 1.0.6] - 2018-08-09
### Added ### Added
* `backend.GetUserSettings()`. * `backend.GetUserSettings()`.
@ -849,7 +902,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Frequent Thunderbird timeout. * Frequent Thunderbird timeout.
* SMTP requests not case-sensitive. * SMTP requests not case-sensitive.
## [v1.0.5] - 2018-07-12 ## [Bridge 1.0.5] - 2018-07-12
### Added ### Added
* UpdateCurrentAgent from lastMailClient. * UpdateCurrentAgent from lastMailClient.
@ -881,7 +934,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Fixed 7bit MIME issue while sending. * Fixed 7bit MIME issue while sending.
## [v1.0.4] - 2018-05-15 ## [Bridge 1.0.4] - 2018-05-15
### Changed ### Changed
* Version files available at both download and static. * Version files available at both download and static.
@ -902,11 +955,11 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Notification that outgoing email will be delivered as non-encrypted. * Notification that outgoing email will be delivered as non-encrypted.
* NOTE: Due to a change of the keychain format, you will need to add your account(s) to the Bridge after installing this version. * NOTE: Due to a change of the keychain format, you will need to add your account(s) to the Bridge after installing this version.
### Bugs fixed ### Fixed bugs
* Support accounts with same user names. * Support accounts with same user names.
* Support sending vCalendar event. * Support sending vCalendar event.
## [v1.0.3] - 2018-03-26 ## [Bridge 1.0.3] - 2018-03-26
* All from silent updates plus following. * All from silent updates plus following.
### Changed ### Changed
@ -941,7 +994,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Remove firewall error message. * Remove firewall error message.
## [v1.0.2] - 2018-03-12 ## [Bridge 1.0.2] - 2018-03-12
* All from silent updates plus following. * All from silent updates plus following.
### Added ### Added
@ -963,7 +1016,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [v1.0.1-4 (linux only)] Silent deploy - 2018-02-28 ## [Bridge 1.0.1-4 (linux only)] Silent deploy - 2018-02-28
### Changed ### Changed
* More similar look of window title bar to Windows 10 style. * More similar look of window title bar to Windows 10 style.
@ -987,14 +1040,14 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [v1.0.1] Silent deploy - 2017-12-30 ## [Bridge 1.0.1] Silent deploy - 2017-12-30
### Changed ### Changed
* Fixed bug with parsing address list (CC became BCC). * Fixed bug with parsing address list (CC became BCC).
## [v1.0.1] - 2017-12-20 ## [Bridge 1.0.1] - 2017-12-20
### Added ### Added
* When current log file is more than 10MB open new one, checked every 15min. * When current log file is more than 10MB open new one, checked every 15min.
@ -1026,7 +1079,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [v1.0.0] - 2017-12-06 ## [Bridge 1.0.0] - 2017-12-06
### Added ### Added
* Encoding support of message body, title items, attachment name, for all standard charsets. * Encoding support of message body, title items, attachment name, for all standard charsets.

View File

@ -10,19 +10,21 @@ TARGET_OS?=${GOOS}
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go .PHONY: build build-ie build-nogui build-ie-nogui check-has-go
# 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.5.0-git BRIDGE_APP_VERSION?=1.5.4-git
IE_APP_VERSION?=1.2.1-git IE_APP_VERSION?=1.2.3-git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns SRC_ICNS:=Bridge.icns
SRC_SVG:=logo.svg SRC_SVG:=logo.svg
TGT_ICNS:=Bridge.icns TGT_ICNS:=Bridge.icns
EXE_NAME:=proton-bridge
ifeq "${TARGET_CMD}" "Import-Export" ifeq "${TARGET_CMD}" "Import-Export"
APP_VERSION:=${IE_APP_VERSION} APP_VERSION:=${IE_APP_VERSION}
SRC_ICO:=ie.ico SRC_ICO:=ie.ico
SRC_ICNS:=ie.icns SRC_ICNS:=ie.icns
SRC_SVG:=ie.svg SRC_SVG:=ie.svg
TGT_ICNS:=ImportExport.icns TGT_ICNS:=ImportExport.icns
EXE_NAME:=proton-ie
endif endif
REVISION:=$(shell git rev-parse --short=10 HEAD) REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z) BUILD_TIME:=$(shell date +%FT%T%z)
@ -40,29 +42,40 @@ BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
ICO_FILES:= ICO_FILES:=
EXE:=$(shell basename ${CURDIR}) DIRNAME:=$(shell basename ${CURDIR})
EXE:=${EXE_NAME}
EXE_QT:=${DIRNAME}
ifeq "${TARGET_OS}" "windows" ifeq "${TARGET_OS}" "windows"
EXE:=${EXE}.exe EXE:=${EXE}.exe
EXE_QT:=${EXE_QT}.exe
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
endif endif
ifeq "${TARGET_OS}" "darwin" ifeq "${TARGET_OS}" "darwin"
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
EXE:=${EXE}.app/Contents/MacOS/${EXE} EXE:=${EXE}.app
EXE_QT:=${EXE_QT}.app
EXE_BINARY_DARWIN:=/Contents/MacOS/${EXE_NAME}
endif endif
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE} EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
EXE_QT_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE_QT}
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
ifeq "${TARGET_CMD}" "Import-Export" ifeq "${TARGET_CMD}" "Import-Export"
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
endif endif
ifdef QT_API
VENDOR_TARGET:=prepare-vendor update-qt-docs
else
VENDOR_TARGET=update-vendor
endif
build: ${TGZ_TARGET} build: ${TGZ_TARGET}
build-ie: build-ie:
TARGET_CMD=Import-Export $(MAKE) build TARGET_CMD=Import-Export $(MAKE) build
build-nogui: build-nogui:
go build ${BUILD_FLAGS_NOGUI} -o ${TARGET_CMD} cmd/${TARGET_CMD}/main.go go build ${BUILD_FLAGS_NOGUI} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
build-ie-nogui: build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui TARGET_CMD=Import-Export $(MAKE) build-nogui
@ -77,12 +90,16 @@ ${DEPLOY_DIR}/linux: ${EXE_TARGET}
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/ cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
${DEPLOY_DIR}/darwin: ${EXE_TARGET} ${DEPLOY_DIR}/darwin: ${EXE_TARGET}
if [ "${DIRNAME}" != "${EXE_NAME}" ]; then \
mv ${EXE_TARGET}/Contents/MacOS/{${DIRNAME},${EXE_NAME}}; \
perl -i -pe"s/>${DIRNAME}/>${EXE_NAME}/g" ${EXE_TARGET}/Contents/Info.plist; \
fi
cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${TGT_ICNS} cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${TGT_ICNS}
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/ cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}" ./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}"
${DEPLOY_DIR}/windows: ${EXE_TARGET} ${DEPLOY_DIR}/windows: ${EXE_TARGET}
cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico
@ -95,11 +112,12 @@ ifneq "${GOOS}" "${TARGET_OS}"
endif endif
endif endif
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor ${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET}
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR} rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
cp cmd/${TARGET_CMD}/main.go . cp cmd/${TARGET_CMD}/main.go .
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET} qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
mv deploy cmd/${TARGET_CMD} mv deploy cmd/${TARGET_CMD}
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
rm -rf ${TARGET_OS} main.go rm -rf ${TARGET_OS} main.go
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO} logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
@ -111,7 +129,7 @@ icon_windows.syso: icon.rc logo.ico
## Rules for therecipe/qt ## Rules for therecipe/qt
.PHONY: prepare-vendor update-vendor .PHONY: prepare-vendor update-vendor update-qt-docs
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513 THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
# vendor folder will be deleted by gomod hence we cache the big repo # vendor folder will be deleted by gomod hence we cache the big repo
@ -136,6 +154,8 @@ prepare-vendor:
update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
${LINKCMD} ${LINKCMD}
update-qt-docs:
go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API)
## Dev dependencies ## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated .PHONY: install-devel-tools install-linter install-go-mod-outdated
@ -196,16 +216,19 @@ coverage: test
mocks: mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager > internal/transfer/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
lint: lint-golang lint-license lint: lint-golang lint-license lint-changelog
lint-license: lint-license:
./utils/missing_license.sh check ./utils/missing_license.sh check
lint-changelog:
./utils/changelog_linter.sh
lint-golang: lint-golang:
which golangci-lint || $(MAKE) install-linter which golangci-lint || $(MAKE) install-linter
golangci-lint run ./... golangci-lint run ./...

View File

@ -3,7 +3,7 @@ Copyright (c) 2020 Proton Technologies AG
This repository holds the ProtonMail Bridge and the ProtonMail Import-Export applications. This repository holds the ProtonMail Bridge and the ProtonMail Import-Export applications.
For a detailed build information see [BUILDS](./BUILDS.md). For a detailed build information see [BUILDS](./BUILDS.md).
For licensing information see [COPYING](./COPYING.md). The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md).
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md). For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).

8
go.mod
View File

@ -6,7 +6,6 @@ go 1.13
// 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-smtp v0.0.0-20180712174835-db5eec195e67
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
) )
@ -18,14 +17,13 @@ require (
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-rfc5322 v0.2.0 github.com/ProtonMail/go-rfc5322 v0.2.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.0.1 github.com/ProtonMail/gopenpgp/v2 v2.0.1
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/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cucumber/godog v0.8.1 github.com/cucumber/godog v0.8.1
@ -39,11 +37,12 @@ require (
github.com/emersion/go-mbox v1.0.2 github.com/emersion/go-mbox v1.0.2
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.14.0
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
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/raven-go v0.2.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.3.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
@ -76,7 +75,6 @@ require (
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-20201102134601-418cd74e9474 github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8 golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8
) )

158
go.sum
View File

@ -1,6 +1,10 @@
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= github.com/Masterminds/semver/v3 v3.1.0 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/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
@ -21,32 +25,38 @@ github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDE
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-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.2.0 h1:tndoDGFtiCvESta9KLUeMksojz8qf76PefnkoQ+fqeg= github.com/ProtonMail/go-rfc5322 v0.2.1 h1:J2PHusboDAYUfE+uBfoJnKZPbnVmzK1zXw6dQrgV8yE=
github.com/ProtonMail/go-rfc5322 v0.2.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU= github.com/ProtonMail/go-rfc5322 v0.2.1/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 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.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU= github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY= github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
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/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA= github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U= github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY= github.com/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/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -57,6 +67,11 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/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/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/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 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
@ -77,73 +92,151 @@ github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1: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-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/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro= github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI= github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= 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/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
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/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/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
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.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
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/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
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.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.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-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
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/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
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/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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg= github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d 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/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/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=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
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/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A= github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
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/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/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo= github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e 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/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.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@ -152,6 +245,14 @@ 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/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -168,29 +269,59 @@ github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLw
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/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk= github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/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/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-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-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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=
@ -201,6 +332,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@ -210,9 +344,17 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/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/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/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-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 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=

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Wed Nov 4 13:57:47 CET 2020. DO NOT EDIT. // Code generated by ./credits.sh at Fri Nov 27 09:23:06 CET 2020. DO NOT EDIT.
package bridge package bridge
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/antlr/antlr4;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -15,17 +15,16 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Wed Nov 4 12:24:35 PM CET 2020'. DO NOT EDIT. // Code generated by ./release-notes.sh at 'Wed Dec 9 05:55:04 PM CET 2020'. DO NOT EDIT.
package bridge package bridge
const ReleaseNotes = `Ensured better message flow by refactoring both address and date parsing const ReleaseNotes = `Support read confirmations
Improved secure connectivity checks Adding GPLv3 licence button to the GUI
Better deb packaging Improved testing
• More robust error handling
` `
const ReleaseFixedBugs = `Ensured that conversations are properly threaded const ReleaseFixedBugs = `AppleMail crashes (timestamp related)
Fixed Linux font issues (Fedora) Encoding errors
Better handling of Mime encrypted messages Installation issues on linux
` `

View File

@ -22,7 +22,8 @@ import (
"runtime" "runtime"
"github.com/ProtonMail/proton-bridge/pkg/constants" "github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/getsentry/raven-go" pkgSentry "github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/getsentry/sentry-go"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -51,10 +52,19 @@ var (
// Main sets up Sentry, filters out unwanted args, creates app and runs it. // Main sets up Sentry, filters out unwanted args, creates app and runs it.
func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) { func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) {
if err := raven.SetDSN(constants.DSNSentry); err != nil { err := sentry.Init(sentry.ClientOptions{
Dsn: constants.DSNSentry,
Release: constants.Revision,
BeforeSend: pkgSentry.EnhanceSentryEvent,
})
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetFingerprint([]string{"{{ default }}"})
})
if err != nil {
log.WithError(err).Errorln("Can not setup sentry DSN") log.WithError(err).Errorln("Can not setup sentry DSN")
} }
raven.SetRelease(constants.Revision)
filterProcessSerialNumberFromArgs() filterProcessSerialNumberFromArgs()
filterRestartNumberFromArgs() filterRestartNumberFromArgs()

View File

@ -26,6 +26,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/frontend" "github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -92,6 +93,8 @@ type PanicHandler struct {
// HandlePanic should be called in defer to ensure restart of app after error. // HandlePanic should be called in defer to ensure restart of app after error.
func (ph *PanicHandler) HandlePanic() { func (ph *PanicHandler) HandlePanic() {
sentry.SkipDuringUnwind()
r := recover() r := recover()
if r == nil { if r == nil {
return return

View File

@ -102,7 +102,7 @@ Item {
Row { Row {
anchors.left : parent.left anchors.left : parent.left
Rectangle { height: Style.dialog.spacing; width: (wrapper.width- credits.width - release.width - sepaCreditsRelease.width)/2; color: "transparent"} Rectangle { height: Style.dialog.spacing; width: (wrapper.width - credits.width - licenseFile.width - release.width - sepaCreditsRelease.width)/2; color: "transparent"}
ClickIconText { ClickIconText {
id:credits id:credits
@ -114,6 +114,20 @@ Item {
onClicked : winMain.dialogCredits.show() onClicked : winMain.dialogCredits.show()
} }
Rectangle {id: sepaLicenseFile ; height: Style.dialog.spacing; width: Style.main.dummy; color: "transparent"}
ClickIconText {
id:licenseFile
iconText : ""
text : qsTr("License", "link to click on to view license file")
textColor : Style.main.textDisabled
fontSize : Style.main.fontSize
textUnderline : true
onClicked : {
go.openLicenseFile()
}
}
Rectangle {id: sepaCreditsRelease ; height: Style.dialog.spacing; width: Style.main.dummy; color: "transparent"} Rectangle {id: sepaCreditsRelease ; height: Style.dialog.spacing; width: Style.main.dummy; color: "transparent"}
ClickIconText { ClickIconText {

View File

@ -355,6 +355,25 @@ Dialog {
InlineLabelSelect { InlineLabelSelect {
id: globalLabels id: globalLabels
} }
Row {
spacing: Style.dialog.spacing
CheckBoxLabel {
id: importEncrypted
text: qsTr("Import encrypted emails as they are")
anchors {
bottom: parent.bottom
bottomMargin: Style.dialog.fontSize/1.8
}
}
InfoToolTip {
anchors {
verticalCenter: importEncrypted.verticalCenter
}
info: qsTr("When this option is enabled, encrypted emails will be imported as ciphertext. Otherwise, such messages will be skipped.", "todo")
}
}
} }
// Buttons // Buttons
@ -1018,7 +1037,7 @@ Dialog {
) )
break break
case DialogImport.Page.Progress: case DialogImport.Page.Progress:
go.startImport(root.address) go.startImport(root.address, importEncrypted.checked)
break break
} }
} }

View File

@ -106,6 +106,20 @@ Item {
} }
} }
Text {
id: licenseFile
text : qsTr("License", "link to click on to open license file")
color : Style.main.textDisabled
font.pointSize: Style.main.fontSize * Style.pt
font.underline: true
MouseArea {
anchors.fill: parent
onClicked : {
go.openLicenseFile()
}
cursorShape: Qt.PointingHandCursor
}
}
Text { Text {
id: releaseNotes id: releaseNotes

View File

@ -45,7 +45,7 @@ Row {
} }
InfoToolTip { InfoToolTip {
info: qsTr( "When master import lablel is selected then all imported email will have this label.", "Tooltip text for master import label") info: qsTr( "When master import label is selected then all imported emails will have this label.", "Tooltip text for master import label")
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }

View File

@ -433,6 +433,10 @@ func (f *FrontendQt) resetSource() {
} }
} }
func (f *FrontendQt) openLicenseFile() {
go open.Run(f.config.GetLicenseFilePath())
}
// getLocalVersionInfo is identical to bridge. // getLocalVersionInfo is identical to bridge.
func (f *FrontendQt) getLocalVersionInfo() { func (f *FrontendQt) getLocalVersionInfo() {
defer f.Qml.ProcessFinished() defer f.Qml.ProcessFinished()

View File

@ -73,7 +73,7 @@ func (f *FrontendQt) loadStructuresForImport() error {
return nil return nil
} }
func (f *FrontendQt) StartImport(email string) { // TODO email not needed func (f *FrontendQt) StartImport(email string, importEncrypted bool) { // TODO email not needed
log.Trace("Starting import") log.Trace("Starting import")
f.Qml.SetProgressDescription("init") // TODO use const f.Qml.SetProgressDescription("init") // TODO use const
@ -84,6 +84,7 @@ func (f *FrontendQt) StartImport(email string) { // TODO email not needed
f.Qml.SetTotal(1) f.Qml.SetTotal(1)
f.Qml.SetImportLogFileName("") f.Qml.SetImportLogFileName("")
f.transfer.SetSkipEncryptedMessages(!importEncrypted)
progress := f.transfer.Start() progress := f.transfer.Start()
f.Qml.SetImportLogFileName(progress.FileReport()) f.Qml.SetImportLogFileName(progress.FileReport())

View File

@ -73,6 +73,7 @@ type GoQMLInterface struct {
_ func(okay bool) `signal:"importStructuresLoadFinished"` _ func(okay bool) `signal:"importStructuresLoadFinished"`
_ func() `signal:"openManual"` _ func() `signal:"openManual"`
_ func(showMessage bool) `signal:"runCheckVersion"` _ func(showMessage bool) `signal:"runCheckVersion"`
_ func() `slot:"openLicenseFile"`
_ func() `slot:"getLocalVersionInfo"` _ func() `slot:"getLocalVersionInfo"`
_ func() `slot:"loadImportReports"` _ func() `slot:"loadImportReports"`
@ -95,7 +96,7 @@ type GoQMLInterface struct {
_ func() string `slot:"leastUsedColor"` _ func() string `slot:"leastUsedColor"`
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"` _ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
_ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"` _ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"`
_ func(email string) `slot:"startImport"` _ func(email string, importEncrypted bool) `slot:"startImport"`
_ func() `slot:"resetSource"` _ func() `slot:"resetSource"`
_ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetAddress string) `slot:"setupAndLoadForImport"` _ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetAddress string) `slot:"setupAndLoadForImport"`
@ -167,6 +168,7 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.SetIsRestarting(false) s.SetIsRestarting(false)
s.SetProgramTitle(f.programName) s.SetProgramTitle(f.programName)
s.ConnectOpenLicenseFile(f.openLicenseFile)
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo) s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable) s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
s.ConnectGetBackendVersion(func() string { s.ConnectGetBackendVersion(func() string {

View File

@ -415,6 +415,10 @@ func (s *FrontendQt) isNewVersionAvailable(showMessage bool) {
}() }()
} }
func (s *FrontendQt) openLicenseFile() {
go open.Run(s.config.GetLicenseFilePath())
}
func (s *FrontendQt) getLocalVersionInfo() { func (s *FrontendQt) getLocalVersionInfo() {
defer s.Qml.ProcessFinished() defer s.Qml.ProcessFinished()
localVersion := s.updates.GetLocalVersion() localVersion := s.updates.GetLocalVersion()

View File

@ -91,6 +91,7 @@ type GoQMLInterface struct {
_ func() `slot:"errorSystray"` _ func() `slot:"errorSystray"`
_ func() `slot:"normalSystray"` _ func() `slot:"normalSystray"`
_ func() `slot:"openLicenseFile"`
_ func() `slot:"getLocalVersionInfo"` _ func() `slot:"getLocalVersionInfo"`
_ func(showMessage bool) `slot:"isNewVersionAvailable"` _ func(showMessage bool) `slot:"isNewVersionAvailable"`
_ func() string `slot:"getBackendVersion"` _ func() string `slot:"getBackendVersion"`
@ -152,6 +153,7 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectClearCache(f.clearCache) s.ConnectClearCache(f.clearCache)
s.ConnectClearKeychain(f.clearKeychain) s.ConnectClearKeychain(f.clearKeychain)
s.ConnectOpenLicenseFile(f.openLicenseFile)
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo) s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable) s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
s.ConnectGetIMAPPort(f.getIMAPPort) s.ConnectGetIMAPPort(f.getIMAPPort)

View File

@ -46,6 +46,9 @@ type imapBackend struct {
imapCache map[string]map[string]string imapCache map[string]map[string]string
imapCachePath string imapCachePath string
imapCacheLock *sync.RWMutex imapCacheLock *sync.RWMutex
updatesBlocking map[string]bool
updatesBlockingLocker sync.Locker
} }
// NewIMAPBackend returns struct implementing go-imap/backend interface. // NewIMAPBackend returns struct implementing go-imap/backend interface.
@ -58,10 +61,6 @@ func NewIMAPBackend(
bridgeWrap := newBridgeWrap(bridge) bridgeWrap := newBridgeWrap(bridge)
backend := newIMAPBackend(panicHandler, cfg, bridgeWrap, eventListener) backend := newIMAPBackend(panicHandler, cfg, bridgeWrap, eventListener)
// We want idle updates coming from bridge's updates channel (which in turn come
// from the bridge users' stores) to be sent to the imap backend's update channel.
backend.updates = bridge.GetIMAPUpdatesChannel()
go backend.monitorDisconnectedUsers() go backend.monitorDisconnectedUsers()
return backend return backend
@ -84,6 +83,9 @@ func newIMAPBackend(
imapCachePath: cfg.GetIMAPCachePath(), imapCachePath: cfg.GetIMAPCachePath(),
imapCacheLock: &sync.RWMutex{}, imapCacheLock: &sync.RWMutex{},
updatesBlocking: map[string]bool{},
updatesBlockingLocker: &sync.Mutex{},
} }
} }
@ -169,7 +171,9 @@ func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMA
// The update channel should be nil until we try to login to IMAP for the first time // The update channel should be nil until we try to login to IMAP for the first time
// so that it doesn't make bridge slow for users who are only using bridge for SMTP // so that it doesn't make bridge slow for users who are only using bridge for SMTP
// (otherwise the store will be locked for 1 sec per email during synchronization). // (otherwise the store will be locked for 1 sec per email during synchronization).
imapUser.user.SetIMAPIdleUpdateChannel() if store := imapUser.user.GetStore(); store != nil {
store.SetChangeNotifier(ib)
}
return imapUser, nil return imapUser, nil
} }

View File

@ -0,0 +1,169 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"strings"
"time"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
imap "github.com/emersion/go-imap"
goIMAPBackend "github.com/emersion/go-imap/backend"
"github.com/sirupsen/logrus"
)
type operation string
const (
operationUpdateMessage operation = "store"
operationDeleteMessage operation = "expunge"
)
func (ib *imapBackend) setUpdatesBeBlocking(address, mailboxName string, op operation) {
ib.changeUpdatesBlocking(address, mailboxName, op, true)
}
func (ib *imapBackend) unsetUpdatesBeBlocking(address, mailboxName string, op operation) {
ib.changeUpdatesBlocking(address, mailboxName, op, false)
}
func (ib *imapBackend) changeUpdatesBlocking(address, mailboxName string, op operation, block bool) {
ib.updatesBlockingLocker.Lock()
defer ib.updatesBlockingLocker.Unlock()
key := strings.ToLower(address + "_" + mailboxName + "_" + string(op))
if block {
ib.updatesBlocking[key] = true
} else {
delete(ib.updatesBlocking, key)
}
}
func (ib *imapBackend) isBlocking(address, mailboxName string, op operation) bool {
key := strings.ToLower(address + "_" + mailboxName + "_" + string(op))
return ib.updatesBlocking[key]
}
func (ib *imapBackend) Notice(address, notice string) {
update := new(goIMAPBackend.StatusUpdate)
update.Update = goIMAPBackend.NewUpdate(address, "")
update.StatusResp = &imap.StatusResp{
Type: imap.StatusRespOk,
Code: imap.CodeAlert,
Info: notice,
}
ib.sendIMAPUpdate(update, false)
}
func (ib *imapBackend) UpdateMessage(
address, mailboxName string,
uid, sequenceNumber uint32,
msg *pmapi.Message, hasDeletedFlag bool,
) {
log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
"seqNum": sequenceNumber,
"uid": uid,
"flags": message.GetFlags(msg),
"deleted": hasDeletedFlag,
}).Trace("IDLE update")
update := new(goIMAPBackend.MessageUpdate)
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
update.Message.Flags = message.GetFlags(msg)
if hasDeletedFlag {
update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag)
}
update.Message.Uid = uid
ib.sendIMAPUpdate(update, ib.isBlocking(address, mailboxName, operationUpdateMessage))
}
func (ib *imapBackend) DeleteMessage(address, mailboxName string, sequenceNumber uint32) {
log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
"seqNum": sequenceNumber,
}).Trace("IDLE delete")
update := new(goIMAPBackend.ExpungeUpdate)
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
update.SeqNum = sequenceNumber
ib.sendIMAPUpdate(update, ib.isBlocking(address, mailboxName, operationDeleteMessage))
}
func (ib *imapBackend) MailboxCreated(address, mailboxName string) {
log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
}).Trace("IDLE mailbox info")
update := new(goIMAPBackend.MailboxInfoUpdate)
update.Update = goIMAPBackend.NewUpdate(address, "")
update.MailboxInfo = &imap.MailboxInfo{
Attributes: []string{imap.NoInferiorsAttr},
Delimiter: store.PathDelimiter,
Name: mailboxName,
}
ib.sendIMAPUpdate(update, false)
}
func (ib *imapBackend) MailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint32) {
log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
"total": total,
"unread": unread,
"unreadSeqNum": unreadSeqNum,
}).Trace("IDLE status")
update := new(goIMAPBackend.MailboxUpdate)
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen})
update.MailboxStatus.Messages = total
update.MailboxStatus.Unseen = unread
update.MailboxStatus.UnseenSeqNum = unreadSeqNum
ib.sendIMAPUpdate(update, false)
}
func (ib *imapBackend) sendIMAPUpdate(update goIMAPBackend.Update, block bool) {
if ib.updates == nil {
log.Trace("IMAP IDLE unavailable")
return
}
done := update.Done()
go func() {
select {
case <-time.After(1 * time.Second):
log.Warn("IMAP update could not be sent (timeout)")
return
case ib.updates <- update:
}
}()
if !block {
return
}
select {
case <-done:
case <-time.After(1 * time.Second):
log.Warn("IMAP update could not be delivered (timeout).")
return
}
}

View File

@ -40,7 +40,6 @@ type bridgeUser interface {
IsCombinedAddressMode() bool IsCombinedAddressMode() bool
GetAddressID(address string) (string, error) GetAddressID(address string) (string, error)
GetPrimaryAddress() string GetPrimaryAddress() string
SetIMAPIdleUpdateChannel()
UpdateUser() error UpdateUser() error
Logout() error Logout() error
CloseConnection(address string) CloseConnection(address string)

View File

@ -18,7 +18,6 @@
package cache package cache
import ( import (
"bytes"
"fmt" "fmt"
"testing" "testing"
"time" "time"
@ -59,34 +58,34 @@ func TestClearOld(t *testing.T) {
} }
func TestClearBig(t *testing.T) { func TestClearBig(t *testing.T) {
msg := []byte("Test message") r := require.New(t)
wantMessage := []byte("Test message")
nSize := 3 wantCacheSize := 3
cacheSizeLimit = nSize*len(msg) + 1 nTestMessages := wantCacheSize * wantCacheSize
cacheTimeLimit = int64(nSize * nSize * 2) // be sure the message will survive cacheSizeLimit = wantCacheSize*len(wantMessage) + 1
cacheTimeLimit = int64(1 << 20) // be sure the message will survive
// It should have more than nSize items. // It should never have more than nSize items.
for i := 0; i < nSize*nSize; i++ { for i := 0; i < nTestMessages; i++ {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
SaveMail(fmt.Sprintf("%s%d", testUID, i), msg, bs) SaveMail(fmt.Sprintf("%s%d", testUID, i), wantMessage, bs)
if len(mailCache) > nSize { r.LessOrEqual(len(mailCache), wantCacheSize, "cache too big when %d", i)
t.Error("Number of items in cache should not be more than", nSize)
}
} }
// Check that the oldest are deleted first. // Check that the oldest are deleted first.
for i := 0; i < nSize*nSize; i++ { for i := 0; i < nTestMessages; i++ {
iUID := fmt.Sprintf("%s%d", testUID, i) iUID := fmt.Sprintf("%s%d", testUID, i)
reader, _ := LoadMail(iUID) reader, _ := LoadMail(iUID)
if i < nSize*(nSize-1) && reader.Len() != 0 { mail := mailCache[iUID]
mail := mailCache[iUID]
t.Error("LoadMail should return empty but have:", mail.data, iUID, mail.key.Timestamp)
}
stored := make([]byte, len(msg))
_, _ = reader.Read(stored)
if i >= nSize*(nSize-1) && !bytes.Equal(stored, msg) { if i < (nTestMessages - wantCacheSize) {
t.Error("LoadMail returned wrong message:", stored, iUID) r.Zero(reader.Len(), "LoadMail should return empty, but have %s for %s time %d ", string(mail.data), iUID, mail.key.Timestamp)
} else {
stored := make([]byte, len(wantMessage))
_, err := reader.Read(stored)
r.NoError(err)
r.Equal(wantMessage, stored, "LoadMail returned wrong message: %s for %s time %d", stored, iUID, mail.key.Timestamp)
} }
} }
} }

View File

@ -118,7 +118,7 @@ func (im *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, err
l.Data["address"] = im.storeAddress.AddressID() l.Data["address"] = im.storeAddress.AddressID()
status := imap.NewMailboxStatus(im.name, items) status := imap.NewMailboxStatus(im.name, items)
status.UidValidity = im.storeMailbox.UIDValidity() status.UidValidity = im.storeMailbox.UIDValidity()
status.PermanentFlags = []string{ status.Flags = []string{
imap.SeenFlag, strings.ToUpper(imap.SeenFlag), imap.SeenFlag, strings.ToUpper(imap.SeenFlag),
imap.FlaggedFlag, strings.ToUpper(imap.FlaggedFlag), imap.FlaggedFlag, strings.ToUpper(imap.FlaggedFlag),
imap.DeletedFlag, strings.ToUpper(imap.DeletedFlag), imap.DeletedFlag, strings.ToUpper(imap.DeletedFlag),
@ -127,6 +127,7 @@ func (im *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, err
message.ThunderbirdJunkFlag, message.ThunderbirdJunkFlag,
message.ThunderbirdNonJunkFlag, message.ThunderbirdNonJunkFlag,
} }
status.PermanentFlags = append([]string{}, status.Flags...)
dbTotal, dbUnread, dbUnreadSeqNum, err := im.storeMailbox.GetCounts() dbTotal, dbUnread, dbUnreadSeqNum, err := im.storeMailbox.GetCounts()
l.WithFields(logrus.Fields{ l.WithFields(logrus.Fields{
@ -177,6 +178,9 @@ func (im *imapMailbox) Check() error {
// Expunge permanently removes all messages that have the \Deleted flag set // Expunge permanently removes all messages that have the \Deleted flag set
// from the currently selected mailbox. // from the currently selected mailbox.
func (im *imapMailbox) Expunge() error { func (im *imapMailbox) Expunge() error {
im.user.backend.setUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
defer im.user.backend.unsetUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
return im.storeMailbox.RemoveDeleted() return im.storeMailbox.RemoveDeleted()
} }

View File

@ -40,6 +40,10 @@ import (
openpgperrors "golang.org/x/crypto/openpgp/errors" openpgperrors "golang.org/x/crypto/openpgp/errors"
) )
var (
rfc822Birthday = time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC) //nolint[gochecknoglobals]
)
type doNotCacheError struct{ e error } type doNotCacheError struct{ e error }
func (dnc *doNotCacheError) Error() string { return dnc.e.Error() } func (dnc *doNotCacheError) Error() string { return dnc.e.Error() }
@ -605,7 +609,7 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (
} }
tmpBuf := &bytes.Buffer{} tmpBuf := &bytes.Buffer{}
mainHeader := message.GetHeader(m) mainHeader := buildHeader(m)
if err = writeHeader(tmpBuf, mainHeader); err != nil { if err = writeHeader(tmpBuf, mainHeader); err != nil {
return return
} }
@ -703,3 +707,23 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (
} }
return structure, msgBody, err return structure, msgBody, err
} }
func buildHeader(msg *pmapi.Message) textproto.MIMEHeader {
header := message.GetHeader(msg)
msgTime := time.Unix(msg.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.
d, err := msg.Header.Date()
if err != nil || d.Before(rfc822Birthday) || msgTime.Before(rfc822Birthday) {
if err != nil || d.IsZero() {
header.Set("X-Original-Date", msgTime.Format(time.RFC1123Z))
} else {
header.Set("X-Original-Date", d.Format(time.RFC1123Z))
}
header.Set("Date", rfc822Birthday.Format(time.RFC1123Z))
}
return header
}

View File

@ -46,6 +46,9 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
// 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()
im.user.backend.setUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationUpdateMessage)
defer im.user.backend.unsetUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationUpdateMessage)
messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet) messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
if err != nil || len(messageIDs) == 0 { if err != nil || len(messageIDs) == 0 {
return err return err

View File

@ -126,7 +126,9 @@ func (s *imapServer) ListenAndServe() {
// Stops the server. // Stops the server.
func (s *imapServer) Close() { func (s *imapServer) Close() {
_ = s.server.Close() if err := s.server.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
}
} }
func (s *imapServer) monitorDisconnectedUsers() { func (s *imapServer) monitorDisconnectedUsers() {
@ -139,7 +141,9 @@ func (s *imapServer) monitorDisconnectedUsers() {
disconnectUser := func(conn imapserver.Conn) { disconnectUser := func(conn imapserver.Conn) {
connUser := conn.Context().User connUser := conn.Context().User
if connUser != nil && strings.EqualFold(connUser.Username(), address) { if connUser != nil && strings.EqualFold(connUser.Username(), address) {
_ = conn.Close() if err := conn.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
}
} }
} }
s.server.ForEachConn(disconnectUser) s.server.ForEachConn(disconnectUser)

View File

@ -43,6 +43,8 @@ type storeUserProvider interface {
parentID string) (*pmapi.Message, []*pmapi.Attachment, error) parentID string) (*pmapi.Message, []*pmapi.Attachment, error)
PauseEventLoop(bool) PauseEventLoop(bool)
SetChangeNotifier(store.ChangeNotifier)
} }
type storeAddressProvider interface { type storeAddressProvider interface {

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Wed Nov 4 13:57:47 CET 2020. DO NOT EDIT. // Code generated by ./credits.sh at Fri Nov 27 09:23:06 CET 2020. DO NOT EDIT.
package importexport package importexport
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/antlr/antlr4;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -15,14 +15,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Wed Nov 11 01:57:14 PM CET 2020'. DO NOT EDIT. // Code generated by ./release-notes.sh at 'Wed Dec 9 05:57:01 PM CET 2020'. DO NOT EDIT.
package importexport package importexport
const ReleaseNotes = `Further improvements to address and date parsing const ReleaseNotes = `Allow an import of already encrypted messages (as cypher text)
Better handling and displaying of skipped messages Cosmetic GUI changes
Improved error reporting Better error handling
` `
const ReleaseFixedBugs = ` const ReleaseFixedBugs = `• Installation issues on linux
` `

View File

@ -27,6 +27,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/confirmer" "github.com/ProtonMail/proton-bridge/pkg/confirmer"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
goSMTPBackend "github.com/emersion/go-smtp" goSMTPBackend "github.com/emersion/go-smtp"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -70,7 +71,7 @@ func newSMTPBackend(
} }
// Login authenticates a user. // Login authenticates a user.
func (sb *smtpBackend) Login(username, password string) (goSMTPBackend.User, error) { func (sb *smtpBackend) Login(_ *goSMTPBackend.ConnectionState, username, password string) (goSMTPBackend.Session, error) {
// Called from go-smtp in goroutines - we need to handle panics for each function. // Called from go-smtp in goroutines - we need to handle panics for each function.
defer sb.panicHandler.HandlePanic() defer sb.panicHandler.HandlePanic()
username = strings.ToLower(username) username = strings.ToLower(username)
@ -97,7 +98,14 @@ func (sb *smtpBackend) Login(username, password string) (goSMTPBackend.User, err
if user.IsCombinedAddressMode() { if user.IsCombinedAddressMode() {
addressID = "" addressID = ""
} }
return newSMTPUser(sb.panicHandler, sb.eventListener, sb, user, addressID) return newSMTPUser(sb.panicHandler, sb.eventListener, sb, user, username, addressID)
}
func (sb *smtpBackend) AnonymousLogin(_ *goSMTPBackend.ConnectionState) (goSMTPBackend.Session, error) {
// Called from go-smtp in goroutines - we need to handle panics for each function.
defer sb.panicHandler.HandlePanic()
return nil, errors.New("anonymous login not supported")
} }
func (sb *smtpBackend) shouldReportOutgoingNoEnc() bool { func (sb *smtpBackend) shouldReportOutgoingNoEnc() bool {

View File

@ -45,7 +45,7 @@ type SendPreferences struct {
// internal emails (including the so-called encrypted-to-outside emails, // internal emails (including the so-called encrypted-to-outside emails,
// which even though meant for external users, they don't really get out of // which even though meant for external users, they don't really get out of
// our platform). If the email is sent unencrypted, no PGP scheme is needed. // our platform). If the email is sent unencrypted, no PGP scheme is needed.
Scheme int Scheme pmapi.PackageFlag
// MIMEType is the MIME type to use for formatting the body of the email // MIMEType is the MIME type to use for formatting the body of the email
// (before encryption/after decryption). The standard possibilities are the // (before encryption/after decryption). The standard possibilities are the
@ -191,8 +191,12 @@ func (b *sendPreferencesBuilder) build() (p SendPreferences) {
p.Scheme = pmapi.PGPMIMEPackage p.Scheme = pmapi.PGPMIMEPackage
} }
case b.shouldSign() && !b.shouldEncrypt() && b.getScheme() == pgpMIME: case b.shouldSign() && !b.shouldEncrypt():
p.Scheme = pmapi.ClearMIMEPackage if b.getScheme() == pgpInline {
p.Scheme = pmapi.ClearPackage
} else {
p.Scheme = pmapi.ClearMIMEPackage
}
default: default:
p.Scheme = pmapi.ClearPackage p.Scheme = pmapi.ClearPackage

View File

@ -41,7 +41,7 @@ func TestPreferencesBuilder(t *testing.T) {
wantEncrypt bool wantEncrypt bool
wantSign bool wantSign bool
wantScheme int wantScheme pmapi.PackageFlag
wantMIMEType string wantMIMEType string
wantPublicKey string wantPublicKey string
}{ }{
@ -254,6 +254,20 @@ func TestPreferencesBuilder(t *testing.T) {
wantMIMEType: "multipart/mixed", wantMIMEType: "multipart/mixed",
}, },
{
name: "external with contact sign enabled and plain text",
contactMeta: &ContactMetadata{MIMEType: "text/plain", Scheme: pgpInline, Sign: true, SignIsSet: true},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: false,
wantSign: true,
wantScheme: pmapi.ClearPackage,
wantMIMEType: "text/plain",
},
{ {
name: "external with sign enabled, sending plaintext, should still send as ClearMIME", name: "external with sign enabled, sending plaintext, should still send as ClearMIME",

View File

@ -51,12 +51,12 @@ func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBacke
s.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server { s.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(address, password) user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil { if err != nil {
return err return err
} }
conn.SetUser(user) conn.SetSession(user)
return nil return nil
}) })
}) })
@ -85,14 +85,16 @@ func (s *smtpServer) ListenAndServe() {
l.Error("SMTP failed: ", err) l.Error("SMTP failed: ", err)
return return
} }
defer s.server.Close() defer s.server.Close() //nolint[errcheck]
l.Info("SMTP server stopped") l.Info("SMTP server stopped")
} }
// Stops the server. // Stops the server.
func (s *smtpServer) Close() { func (s *smtpServer) Close() {
s.server.Close() if err := s.server.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
}
} }
func (s *smtpServer) monitorDisconnectedUsers() { func (s *smtpServer) monitorDisconnectedUsers() {
@ -102,9 +104,11 @@ func (s *smtpServer) monitorDisconnectedUsers() {
for address := range ch { for address := range ch {
log.Info("Disconnecting all open SMTP connections for ", address) log.Info("Disconnecting all open SMTP connections for ", address)
disconnectUser := func(conn *goSMTP.Conn) { disconnectUser := func(conn *goSMTP.Conn) {
connUser := conn.User() connUser := conn.Session()
if connUser != nil { if connUser != nil {
_ = conn.Close() if err := conn.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
}
} }
} }
s.server.ForEachConn(disconnectUser) s.server.ForEachConn(disconnectUser)

View File

@ -44,7 +44,11 @@ type smtpUser struct {
backend *smtpBackend backend *smtpBackend
user bridgeUser user bridgeUser
storeUser storeUserProvider storeUser storeUserProvider
username string
addressID string addressID string
from string
to []string
} }
// newSMTPUser returns struct implementing go-smtp/session interface. // newSMTPUser returns struct implementing go-smtp/session interface.
@ -53,8 +57,9 @@ func newSMTPUser(
eventListener listener.Listener, eventListener listener.Listener,
smtpBackend *smtpBackend, smtpBackend *smtpBackend,
user bridgeUser, user bridgeUser,
username string,
addressID string, addressID string,
) (goSMTPBackend.User, error) { ) (goSMTPBackend.Session, error) {
storeUser := user.GetStore() storeUser := user.GetStore()
if storeUser == nil { if storeUser == nil {
return nil, errors.New("user database is not initialized") return nil, errors.New("user database is not initialized")
@ -66,6 +71,7 @@ func newSMTPUser(
backend: smtpBackend, backend: smtpBackend,
user: user, user: user,
storeUser: storeUser, storeUser: storeUser,
username: username,
addressID: addressID, addressID: addressID,
}, nil }, nil
} }
@ -145,6 +151,55 @@ func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey,
return su.client().GetPublicKeysForEmail(recipient) return su.client().GetPublicKeysForEmail(recipient)
} }
// Discard currently processed message.
func (su *smtpUser) Reset() {
log.Trace("Resetting the session")
su.from = ""
su.to = []string{}
}
// Set return path for currently processed message.
func (su *smtpUser) Mail(from string, opts goSMTPBackend.MailOptions) error {
log.WithField("from", from).WithField("opts", opts).Trace("Setting mail from")
// REQUIRETLS and SMTPUTF8 have to be announced to be used by client.
// Bridge does not use those extensions so this should not happen.
if opts.RequireTLS {
return errors.New("REQUIRETLS extension is not supported")
}
if opts.UTF8 {
return errors.New("SMTPUTF8 extension is not supported")
}
if opts.Auth != nil && *opts.Auth != "" && *opts.Auth != su.username {
return errors.New("changing identity is not supported")
}
su.from = from
return nil
}
// Add recipient for currently processed message.
func (su *smtpUser) Rcpt(to string) error {
log.WithField("to", to).Trace("Adding recipient")
if to != "" {
su.to = append(su.to, to)
}
return nil
}
// Set currently processed message contents and send it.
func (su *smtpUser) Data(r io.Reader) error {
log.Trace("Sending the message")
if su.from == "" {
return errors.New("missing sender")
}
if len(su.to) == 0 {
return errors.New("missing recipient")
}
return su.Send(su.from, su.to, r)
}
// Send sends an email from the given address to the given addresses with the given body. // Send sends an email from the given address to the given addresses with the given body.
func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err error) { //nolint[funlen] func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err error) { //nolint[funlen]
// Called from go-smtp in goroutines - we need to handle panics for each function. // Called from go-smtp in goroutines - we need to handle panics for each function.
@ -187,7 +242,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
log.WithError(err).Error("Failed to parse message") log.WithError(err).Error("Failed to parse message")
return return
} }
clearBody := message.Body richBody := message.Body
externalID := message.Header.Get("Message-Id") externalID := message.Header.Get("Message-Id")
externalID = strings.Trim(externalID, "<>") externalID = strings.Trim(externalID, "<>")
@ -256,7 +311,6 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
atts = append(atts, message.Attachments...) atts = append(atts, message.Attachments...)
// Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys. // Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys.
attkeys := make(map[string]*crypto.SessionKey) attkeys := make(map[string]*crypto.SessionKey)
attkeysEncoded := make(map[string]pmapi.AlgoKey)
for _, att := range atts { for _, att := range atts {
var keyPackets []byte var keyPackets []byte
@ -266,23 +320,9 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
if attkeys[att.ID], err = kr.DecryptSessionKey(keyPackets); err != nil { if attkeys[att.ID], err = kr.DecryptSessionKey(keyPackets); err != nil {
return errors.Wrap(err, "decrypting attachment session key") return errors.Wrap(err, "decrypting attachment session key")
} }
attkeysEncoded[att.ID] = pmapi.AlgoKey{
Key: attkeys[att.ID].GetBase64Key(),
Algorithm: attkeys[att.ID].Algo,
}
} }
plainSharedScheme := 0 req := pmapi.NewSendMessageReq(kr, mimeBody, plainBody, richBody, attkeys)
htmlSharedScheme := 0
mimeSharedType := 0
plainAddressMap := make(map[string]*pmapi.MessageAddress)
htmlAddressMap := make(map[string]*pmapi.MessageAddress)
mimeAddressMap := make(map[string]*pmapi.MessageAddress)
var plainKey, htmlKey, mimeKey *crypto.SessionKey
var plainData, htmlData, mimeData []byte
containsUnencryptedRecipients := false containsUnencryptedRecipients := false
for _, email := range to { for _, email := range to {
@ -298,61 +338,15 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
return err return err
} }
var signature int var signature pmapi.SignatureFlag
if sendPreferences.Sign { if sendPreferences.Sign {
signature = pmapi.YesSignature signature = pmapi.SignatureDetached
} else { } else {
signature = pmapi.NoSignature signature = pmapi.SignatureNone
} }
if sendPreferences.Scheme == pmapi.PGPMIMEPackage || sendPreferences.Scheme == pmapi.ClearMIMEPackage {
if mimeKey == nil { if err := req.AddRecipient(email, sendPreferences.Scheme, sendPreferences.PublicKey, signature, sendPreferences.MIMEType, sendPreferences.Encrypt); err != nil {
if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil { return errors.Wrap(err, "failed to add recipient")
return err
}
}
if sendPreferences.Scheme == pmapi.PGPMIMEPackage {
mimeBodyPacket, _, err := createPackets(sendPreferences.PublicKey, mimeKey, map[string]*crypto.SessionKey{})
if err != nil {
return err
}
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature}
} else {
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
}
mimeSharedType |= sendPreferences.Scheme
} else {
switch sendPreferences.MIMEType {
case pmapi.ContentTypePlainText:
if plainKey == nil {
if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil {
return err
}
}
newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
if sendPreferences.Encrypt && sendPreferences.PublicKey != nil {
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, plainKey, attkeys)
if err != nil {
return err
}
}
plainAddressMap[email] = newAddress
plainSharedScheme |= sendPreferences.Scheme
case pmapi.ContentTypeHTML:
if htmlKey == nil {
if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil {
return err
}
}
newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
if sendPreferences.Encrypt && sendPreferences.PublicKey != nil {
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, htmlKey, attkeys)
if err != nil {
return err
}
}
htmlAddressMap[email] = newAddress
htmlSharedScheme |= sendPreferences.Scheme
}
} }
} }
@ -370,31 +364,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
} }
} }
req := &pmapi.SendMessageReq{} req.PreparePackages()
plainPkg := buildPackage(plainAddressMap, plainSharedScheme, pmapi.ContentTypePlainText, plainData, plainKey, attkeysEncoded)
if plainPkg != nil {
req.Packages = append(req.Packages, plainPkg)
}
htmlPkg := buildPackage(htmlAddressMap, htmlSharedScheme, pmapi.ContentTypeHTML, htmlData, htmlKey, attkeysEncoded)
if htmlPkg != nil {
req.Packages = append(req.Packages, htmlPkg)
}
if len(mimeAddressMap) > 0 {
pkg := &pmapi.MessagePackage{
Body: base64.StdEncoding.EncodeToString(mimeData),
Addresses: mimeAddressMap,
MIMEType: pmapi.ContentTypeMultipartMixed,
Type: mimeSharedType,
BodyKey: pmapi.AlgoKey{
Key: mimeKey.GetBase64Key(),
Algorithm: mimeKey.Algo,
},
}
req.Packages = append(req.Packages, pkg)
}
return su.storeUser.SendMessage(message.ID, req) return su.storeUser.SendMessage(message.ID, req)
} }

View File

@ -18,11 +18,7 @@
package smtp package smtp
import ( import (
"encoding/base64"
"regexp" "regexp"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
//nolint:gochecknoglobals // Used like a constant //nolint:gochecknoglobals // Used like a constant
@ -35,85 +31,3 @@ var mailFormat = regexp.MustCompile(`.+@.+\..+`)
func looksLikeEmail(e string) bool { func looksLikeEmail(e string) bool {
return mailFormat.MatchString(e) return mailFormat.MatchString(e)
} }
func createPackets(
pubkey *crypto.KeyRing,
bodyKey *crypto.SessionKey,
attkeys map[string]*crypto.SessionKey,
) (bodyPacket string, attachmentPackets map[string]string, err error) {
// Encrypt message body keys.
packetBytes, err := pubkey.EncryptSessionKey(bodyKey)
if err != nil {
return
}
bodyPacket = base64.StdEncoding.EncodeToString(packetBytes)
// Encrypt attachment keys.
attachmentPackets = make(map[string]string)
for id, attkey := range attkeys {
var packets []byte
if packets, err = pubkey.EncryptSessionKey(attkey); err != nil {
return
}
attachmentPackets[id] = base64.StdEncoding.EncodeToString(packets)
}
return
}
func encryptSymmetric(
kr *crypto.KeyRing,
textToEncrypt string,
canonicalizeText bool, // nolint[unparam]
) (key *crypto.SessionKey, symEncryptedData []byte, err error) {
// We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones).
firstKey, err := kr.FirstKey()
if err != nil {
return
}
pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr)
if err != nil {
return
}
pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0)
if err != nil {
return
}
key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket())
if err != nil {
return
}
symEncryptedData = pgpSplitMessage.GetBinaryDataPacket()
return
}
func buildPackage(
addressMap map[string]*pmapi.MessageAddress,
sharedScheme int,
mimeType string,
bodyData []byte,
bodyKey *crypto.SessionKey,
attKeys map[string]pmapi.AlgoKey,
) (pkg *pmapi.MessagePackage) {
if len(addressMap) == 0 {
return nil
}
pkg = &pmapi.MessagePackage{
Body: base64.StdEncoding.EncodeToString(bodyData),
Addresses: addressMap,
MIMEType: mimeType,
Type: sharedScheme,
}
if sharedScheme|pmapi.ClearPackage > 0 {
pkg.BodyKey.Key = bodyKey.GetBase64Key()
pkg.BodyKey.Algorithm = bodyKey.Algo
pkg.AttachmentKeys = attKeys
}
return pkg
}

View File

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

View File

@ -18,119 +18,56 @@
package store package store
import ( import (
"time"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imap "github.com/emersion/go-imap"
imapBackend "github.com/emersion/go-imap/backend"
"github.com/sirupsen/logrus"
) )
// SetIMAPUpdateChannel sets the channel on which imap update messages will be sent. This should be the channel type ChangeNotifier interface {
// on which the imap backend listens for imap updates. Notice(address, notice string)
func (store *Store) SetIMAPUpdateChannel(updates chan imapBackend.Update) { UpdateMessage(
store.log.Debug("Listening for IMAP updates") address, mailboxName string,
uid, sequenceNumber uint32,
if store.imapUpdates = updates; store.imapUpdates == nil { msg *pmapi.Message, hasDeletedFlag bool)
store.log.Error("The IMAP Updates channel is nil") DeleteMessage(address, mailboxName string, sequenceNumber uint32)
} MailboxCreated(address, mailboxName string)
MailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint32)
} }
func (store *Store) imapNotice(address, notice string) *imapBackend.StatusUpdate { // SetChangeNotifier sets notifier to be called once mailbox or message changes.
update := new(imapBackend.StatusUpdate) func (store *Store) SetChangeNotifier(notifier ChangeNotifier) {
update.Update = imapBackend.NewUpdate(address, "") store.notifier = notifier
update.StatusResp = &imap.StatusResp{
Type: imap.StatusRespOk,
Code: imap.CodeAlert,
Info: notice,
}
store.imapSendUpdate(update)
return update
} }
func (store *Store) imapUpdateMessage( func (store *Store) notifyNotice(address, notice string) {
address, mailboxName string, if store.notifier == nil {
uid, sequenceNumber uint32,
msg *pmapi.Message, hasDeletedFlag bool,
) *imapBackend.MessageUpdate {
store.log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
"seqNum": sequenceNumber,
"uid": uid,
"flags": message.GetFlags(msg),
"deleted": hasDeletedFlag,
}).Trace("IDLE update")
update := new(imapBackend.MessageUpdate)
update.Update = imapBackend.NewUpdate(address, mailboxName)
update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
update.Message.Flags = message.GetFlags(msg)
if hasDeletedFlag {
update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag)
}
update.Message.Uid = uid
store.imapSendUpdate(update)
return update
}
func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumber uint32) *imapBackend.ExpungeUpdate {
store.log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
"seqNum": sequenceNumber,
}).Trace("IDLE delete")
update := new(imapBackend.ExpungeUpdate)
update.Update = imapBackend.NewUpdate(address, mailboxName)
update.SeqNum = sequenceNumber
store.imapSendUpdate(update)
return update
}
func (store *Store) imapMailboxCreated(address, mailboxName string) *imapBackend.MailboxInfoUpdate {
store.log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
}).Trace("IDLE mailbox info")
update := new(imapBackend.MailboxInfoUpdate)
update.Update = imapBackend.NewUpdate(address, "")
update.MailboxInfo = &imap.MailboxInfo{
Attributes: []string{imap.NoInferiorsAttr},
Delimiter: PathDelimiter,
Name: mailboxName,
}
store.imapSendUpdate(update)
return update
}
func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) *imapBackend.MailboxUpdate {
store.log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
"total": total,
"unread": unread,
"unreadSeqNum": unreadSeqNum,
}).Trace("IDLE status")
update := new(imapBackend.MailboxUpdate)
update.Update = imapBackend.NewUpdate(address, mailboxName)
update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen})
update.MailboxStatus.Messages = uint32(total)
update.MailboxStatus.Unseen = uint32(unread)
update.MailboxStatus.UnseenSeqNum = uint32(unreadSeqNum)
store.imapSendUpdate(update)
return update
}
func (store *Store) imapSendUpdate(update imapBackend.Update) {
if store.imapUpdates == nil {
store.log.Trace("IMAP IDLE unavailable")
return return
} }
store.notifier.Notice(address, notice)
select { }
case <-time.After(1 * time.Second):
store.log.Warn("IMAP update could not be sent (timeout)") func (store *Store) notifyUpdateMessage(address, mailboxName string, uid, sequenceNumber uint32, msg *pmapi.Message, hasDeletedFlag bool) {
return if store.notifier == nil {
case store.imapUpdates <- update: return
} }
store.notifier.UpdateMessage(address, mailboxName, uid, sequenceNumber, msg, hasDeletedFlag)
}
func (store *Store) notifyDeleteMessage(address, mailboxName string, sequenceNumber uint32) {
if store.notifier == nil {
return
}
store.notifier.DeleteMessage(address, mailboxName, sequenceNumber)
}
func (store *Store) notifyMailboxCreated(address, mailboxName string) {
if store.notifier == nil {
return
}
store.notifier.MailboxCreated(address, mailboxName)
}
func (store *Store) notifyMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) {
if store.notifier == nil {
return
}
store.notifier.MailboxStatus(address, mailboxName, uint32(total), uint32(unread), uint32(unreadSeqNum))
} }

View File

@ -21,52 +21,43 @@ import (
"testing" "testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestCreateOrUpdateMessageIMAPUpdates(t *testing.T) { func TestNotifyChangeCreateOrUpdateMessage(t *testing.T) {
m, clear := initMocks(t) m, clear := initMocks(t)
defer clear() defer clear()
updates := make(chan imapBackend.Update) m.changeNotifier.EXPECT().MailboxStatus(addr1, "All Mail", uint32(1), uint32(0), uint32(0))
m.changeNotifier.EXPECT().MailboxStatus(addr1, "All Mail", uint32(2), uint32(0), uint32(0))
m.changeNotifier.EXPECT().UpdateMessage(addr1, "All Mail", uint32(1), uint32(1), gomock.Any(), false)
m.changeNotifier.EXPECT().UpdateMessage(addr1, "All Mail", uint32(2), uint32(2), gomock.Any(), false)
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.store.SetIMAPUpdateChannel(updates) m.store.SetChangeNotifier(m.changeNotifier)
go checkIMAPUpdates(t, updates, []func(interface{}) bool{
checkMessageUpdate(addr1, "All Mail", 1, 1),
checkMessageUpdate(addr1, "All Mail", 2, 2),
})
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel})
close(updates)
} }
func TestCreateOrUpdateMessageIMAPUpdatesBulkUpdate(t *testing.T) { func TestNotifyChangeCreateOrUpdateMessages(t *testing.T) {
m, clear := initMocks(t) m, clear := initMocks(t)
defer clear() defer clear()
updates := make(chan imapBackend.Update) m.changeNotifier.EXPECT().MailboxStatus(addr1, "All Mail", uint32(2), uint32(0), uint32(0))
m.changeNotifier.EXPECT().UpdateMessage(addr1, "All Mail", uint32(1), uint32(1), gomock.Any(), false)
m.changeNotifier.EXPECT().UpdateMessage(addr1, "All Mail", uint32(2), uint32(2), gomock.Any(), false)
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.store.SetIMAPUpdateChannel(updates) m.store.SetChangeNotifier(m.changeNotifier)
go checkIMAPUpdates(t, updates, []func(interface{}) bool{
checkMessageUpdate(addr1, "All Mail", 1, 1),
checkMessageUpdate(addr1, "All Mail", 2, 2),
})
msg1 := getTestMessage("msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) msg1 := getTestMessage("msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel})
msg2 := getTestMessage("msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) msg2 := getTestMessage("msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel})
require.Nil(t, m.store.createOrUpdateMessagesEvent([]*pmapi.Message{msg1, msg2})) require.Nil(t, m.store.createOrUpdateMessagesEvent([]*pmapi.Message{msg1, msg2}))
close(updates)
} }
func TestDeleteMessageIMAPUpdate(t *testing.T) { func TestNotifyChangeDeleteMessage(t *testing.T) {
m, clear := initMocks(t) m, clear := initMocks(t)
defer clear() defer clear()
@ -75,55 +66,10 @@ func TestDeleteMessageIMAPUpdate(t *testing.T) {
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel})
updates := make(chan imapBackend.Update) m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(2))
m.store.SetIMAPUpdateChannel(updates) m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(1))
go checkIMAPUpdates(t, updates, []func(interface{}) bool{
checkMessageDelete(addr1, "All Mail", 2),
checkMessageDelete(addr1, "All Mail", 1),
})
m.store.SetChangeNotifier(m.changeNotifier)
require.Nil(t, m.store.deleteMessageEvent("msg2")) require.Nil(t, m.store.deleteMessageEvent("msg2"))
require.Nil(t, m.store.deleteMessageEvent("msg1")) require.Nil(t, m.store.deleteMessageEvent("msg1"))
close(updates)
}
func checkIMAPUpdates(t *testing.T, updates chan imapBackend.Update, checkFunctions []func(interface{}) bool) {
idx := 0
for update := range updates {
if idx >= len(checkFunctions) {
continue
}
if !checkFunctions[idx](update) {
continue
}
idx++
}
require.True(t, idx == len(checkFunctions), "Less updates than expected: %+v of %+v", idx, len(checkFunctions))
}
func checkMessageUpdate(username, mailbox string, seqNum, uid int) func(interface{}) bool { //nolint[unparam]
return func(update interface{}) bool {
switch u := update.(type) {
case *imapBackend.MessageUpdate:
return (u.Update.Username() == username &&
u.Update.Mailbox() == mailbox &&
u.Message.SeqNum == uint32(seqNum) &&
u.Message.Uid == uint32(uid))
default:
return false
}
}
}
func checkMessageDelete(username, mailbox string, seqNum int) func(interface{}) bool { //nolint[unparam]
return func(update interface{}) bool {
switch u := update.(type) {
case *imapBackend.ExpungeUpdate:
return (u.Update.Username() == username &&
u.Update.Mailbox() == mailbox &&
u.SeqNum == uint32(seqNum))
default:
return false
}
}
} }

View File

@ -571,7 +571,7 @@ func (loop *eventLoop) processNotices(l *logrus.Entry, notices []string) {
for _, notice := range notices { for _, notice := range notices {
l.Infof("Notice: %q", notice) l.Infof("Notice: %q", notice)
for _, address := range loop.user.GetStoreAddresses() { for _, address := range loop.user.GetStoreAddresses() {
loop.store.imapNotice(address, notice) loop.store.notifyNotice(address, notice)
} }
} }
} }

View File

@ -24,7 +24,6 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -54,10 +53,10 @@ func TestEventLoopProcessMoreEvents(t *testing.T) {
}, nil), }, nil),
) )
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.client.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
// Event loop runs in goroutine and will be stopped by deferred mock clearing. // Event loop runs in goroutine started during store creation (newStoreNoEvents).
go m.store.eventLoop.start() // Force to run the next event.
m.store.eventLoop.pollNow()
// More events are processed right away. // More events are processed right away.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
@ -78,38 +77,42 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) {
subject := "old subject" subject := "old subject"
newSubject := "new subject" newSubject := "new subject"
// First sync will add message with old subject to database. m.newStoreNoEvents(true, &pmapi.Message{
m.client.EXPECT().GetMessage("msg1").Return(&pmapi.Message{
ID: "msg1", ID: "msg1",
Subject: subject, Subject: subject,
}, nil) })
// Event will update the subject.
m.client.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{
EventID: "event1",
Messages: []*pmapi.EventMessage{{
EventItem: pmapi.EventItem{
ID: "msg1",
Action: pmapi.EventUpdate,
},
Updated: &pmapi.EventMessageUpdated{
ID: "msg1",
Subject: &newSubject,
},
}},
}, nil)
m.newStoreNoEvents(true) eventReceived := make(chan struct{})
m.client.EXPECT().GetEvent("latestEventID").DoAndReturn(func(eventID string) (*pmapi.Event, error) {
defer close(eventReceived)
return &pmapi.Event{
EventID: "event1",
Messages: []*pmapi.EventMessage{{
EventItem: pmapi.EventItem{
ID: "msg1",
Action: pmapi.EventUpdate,
},
Updated: &pmapi.EventMessageUpdated{
ID: "msg1",
Subject: &newSubject,
},
}},
}, nil
})
// Event loop runs in goroutine and will be stopped by deferred mock clearing. // Event loop runs in goroutine started during store creation (newStoreNoEvents).
go m.store.eventLoop.start() // Force to run the next event.
m.store.eventLoop.pollNow()
var err error select {
assert.Eventually(t, func() bool { case <-eventReceived:
var msg *pmapi.Message case <-time.After(5 * time.Second):
msg, err = m.store.getMessageFromDB("msg1") require.Fail(t, "latestEventID was not processed")
return err == nil && msg.Subject == newSubject }
}, time.Second, 10*time.Millisecond)
msg, err := m.store.getMessageFromDB("msg1")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, newSubject, msg.Subject)
} }
func TestEventLoopUpdateMessage(t *testing.T) { func TestEventLoopUpdateMessage(t *testing.T) {

View File

@ -18,8 +18,6 @@
package store package store
import ( import (
"time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -233,6 +231,7 @@ func (storeMailbox *Mailbox) RemoveDeleted() error {
return err return err
} }
case pmapi.DraftLabel: case pmapi.DraftLabel:
storeMailbox.log.WithField("ids", apiIDs).Warn("Deleting drafts")
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil { if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
return err return err
} }
@ -280,6 +279,7 @@ func (storeMailbox *Mailbox) deleteFromTrashOrSpam(apiIDs []string) error {
} }
} }
if len(messageIDsToDelete) > 0 { if len(messageIDsToDelete) > 0 {
storeMailbox.log.WithField("ids", messageIDsToDelete).Warn("Deleting messages")
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil { if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
return err return err
} }
@ -356,7 +356,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
} }
isMarkedAsDeleted := deletedBucket.Get([]byte(msg.ID)) != nil isMarkedAsDeleted := deletedBucket.Get([]byte(msg.ID)) != nil
if seqErr == nil { if seqErr == nil {
storeMailbox.store.imapUpdateMessage( storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address, storeMailbox.storeAddress.address,
storeMailbox.labelName, storeMailbox.labelName,
btoi(uidb), btoi(uidb),
@ -390,7 +390,7 @@ 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.imapUpdateMessage( storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address, storeMailbox.storeAddress.address,
storeMailbox.labelName, storeMailbox.labelName,
uid, uid,
@ -441,7 +441,7 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
} }
if seqNumErr == nil { if seqNumErr == nil {
storeMailbox.store.imapDeleteMessage( storeMailbox.store.notifyDeleteMessage(
storeMailbox.storeAddress.address, storeMailbox.storeAddress.address,
storeMailbox.labelName, storeMailbox.labelName,
seqNum, seqNum,
@ -459,7 +459,7 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
if err != nil { if err != nil {
return errors.Wrap(err, "cannot get counts for mailbox status update") return errors.Wrap(err, "cannot get counts for mailbox status update")
} }
storeMailbox.store.imapMailboxStatus( storeMailbox.store.notifyMailboxStatus(
storeMailbox.storeAddress.address, storeMailbox.storeAddress.address,
storeMailbox.labelName, storeMailbox.labelName,
total, total,
@ -503,7 +503,7 @@ func (storeMailbox *Mailbox) txMarkMessagesAsDeleted(tx *bolt.Tx, apiIDs []strin
// In order to send flags in format // In order to send flags in format
// S: * 2 FETCH (FLAGS (\Deleted \Seen)) // S: * 2 FETCH (FLAGS (\Deleted \Seen))
update := storeMailbox.store.imapUpdateMessage( storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address, storeMailbox.storeAddress.address,
storeMailbox.labelName, storeMailbox.labelName,
uid, uid,
@ -511,14 +511,6 @@ func (storeMailbox *Mailbox) txMarkMessagesAsDeleted(tx *bolt.Tx, apiIDs []strin
msg, msg,
markAsDeleted, markAsDeleted,
) )
// txMarkMessagesAsDeleted is called only during processing request
// from IMAP call (i.e., not from event loop) and in such cases we
// have to wait to propagate update back before closing the response.
select {
case <-time.After(1 * time.Second):
case <-update.Done():
}
} }
return nil return nil

View File

@ -1,14 +1,13 @@
// 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) // Source: github.com/ProtonMail/proton-bridge/internal/store (interfaces: PanicHandler,ClientManager,BridgeUser,ChangeNotifier)
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
import ( import (
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"
reflect "reflect"
) )
// MockPanicHandler is a mock of PanicHandler interface // MockPanicHandler is a mock of PanicHandler interface
@ -242,3 +241,86 @@ func (mr *MockBridgeUserMockRecorder) UpdateUser() *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))
} }
// MockChangeNotifier is a mock of ChangeNotifier interface
type MockChangeNotifier struct {
ctrl *gomock.Controller
recorder *MockChangeNotifierMockRecorder
}
// MockChangeNotifierMockRecorder is the mock recorder for MockChangeNotifier
type MockChangeNotifierMockRecorder struct {
mock *MockChangeNotifier
}
// NewMockChangeNotifier creates a new mock instance
func NewMockChangeNotifier(ctrl *gomock.Controller) *MockChangeNotifier {
mock := &MockChangeNotifier{ctrl: ctrl}
mock.recorder = &MockChangeNotifierMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockChangeNotifier) EXPECT() *MockChangeNotifierMockRecorder {
return m.recorder
}
// DeleteMessage mocks base method
func (m *MockChangeNotifier) DeleteMessage(arg0, arg1 string, arg2 uint32) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "DeleteMessage", arg0, arg1, arg2)
}
// DeleteMessage indicates an expected call of DeleteMessage
func (mr *MockChangeNotifierMockRecorder) DeleteMessage(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockChangeNotifier)(nil).DeleteMessage), arg0, arg1, arg2)
}
// MailboxCreated mocks base method
func (m *MockChangeNotifier) MailboxCreated(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "MailboxCreated", arg0, arg1)
}
// MailboxCreated indicates an expected call of MailboxCreated
func (mr *MockChangeNotifierMockRecorder) MailboxCreated(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxCreated", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxCreated), arg0, arg1)
}
// MailboxStatus mocks base method
func (m *MockChangeNotifier) MailboxStatus(arg0, arg1 string, arg2, arg3, arg4 uint32) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "MailboxStatus", arg0, arg1, arg2, arg3, arg4)
}
// MailboxStatus indicates an expected call of MailboxStatus
func (mr *MockChangeNotifierMockRecorder) MailboxStatus(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxStatus", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxStatus), arg0, arg1, arg2, arg3, arg4)
}
// Notice mocks base method
func (m *MockChangeNotifier) Notice(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Notice", arg0, arg1)
}
// Notice indicates an expected call of Notice
func (mr *MockChangeNotifierMockRecorder) Notice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notice", reflect.TypeOf((*MockChangeNotifier)(nil).Notice), arg0, arg1)
}
// UpdateMessage mocks base method
func (m *MockChangeNotifier) UpdateMessage(arg0, arg1 string, arg2, arg3 uint32, arg4 *pmapi.Message, arg5 bool) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UpdateMessage", arg0, arg1, arg2, arg3, arg4, arg5)
}
// UpdateMessage indicates an expected call of UpdateMessage
func (mr *MockChangeNotifierMockRecorder) UpdateMessage(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMessage", reflect.TypeOf((*MockChangeNotifier)(nil).UpdateMessage), arg0, arg1, arg2, arg3, arg4, arg5)
}

View File

@ -26,7 +26,6 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -100,12 +99,12 @@ type Store struct {
log *logrus.Entry log *logrus.Entry
cache *Cache cache *Cache
filePath string filePath string
db *bolt.DB db *bolt.DB
lock *sync.RWMutex lock *sync.RWMutex
addresses map[string]*Address addresses map[string]*Address
imapUpdates chan imapBackend.Update notifier ChangeNotifier
isSyncRunning bool isSyncRunning bool
syncCooldown cooldown syncCooldown cooldown

View File

@ -18,11 +18,12 @@
package store package store
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"testing" "testing"
"time"
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"
@ -43,13 +44,14 @@ const (
type mocksForStore struct { type mocksForStore struct {
tb testing.TB tb testing.TB
ctrl *gomock.Controller ctrl *gomock.Controller
events *storemocks.MockListener events *storemocks.MockListener
user *storemocks.MockBridgeUser user *storemocks.MockBridgeUser
client *pmapimocks.MockClient client *pmapimocks.MockClient
clientManager *storemocks.MockClientManager clientManager *storemocks.MockClientManager
panicHandler *storemocks.MockPanicHandler panicHandler *storemocks.MockPanicHandler
store *Store changeNotifier *storemocks.MockChangeNotifier
store *Store
tmpDir string tmpDir string
cache *Cache cache *Cache
@ -58,13 +60,14 @@ type mocksForStore struct {
func initMocks(tb testing.TB) (*mocksForStore, func()) { func initMocks(tb testing.TB) (*mocksForStore, func()) {
ctrl := gomock.NewController(tb) ctrl := gomock.NewController(tb)
mocks := &mocksForStore{ mocks := &mocksForStore{
tb: tb, tb: tb,
ctrl: ctrl, ctrl: ctrl,
events: storemocks.NewMockListener(ctrl), events: storemocks.NewMockListener(ctrl),
user: storemocks.NewMockBridgeUser(ctrl), user: storemocks.NewMockBridgeUser(ctrl),
client: pmapimocks.NewMockClient(ctrl), client: pmapimocks.NewMockClient(ctrl),
clientManager: storemocks.NewMockClientManager(ctrl), clientManager: storemocks.NewMockClientManager(ctrl),
panicHandler: storemocks.NewMockPanicHandler(ctrl), panicHandler: storemocks.NewMockPanicHandler(ctrl),
changeNotifier: storemocks.NewMockChangeNotifier(ctrl),
} }
// Called during clean-up. // Called during clean-up.
@ -89,7 +92,7 @@ func initMocks(tb testing.TB) (*mocksForStore, func()) {
} }
} }
func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unparam] func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool, msgs ...*pmapi.Message) { //nolint[unparam]
mocks.user.EXPECT().ID().Return("userID").AnyTimes() mocks.user.EXPECT().ID().Return("userID").AnyTimes()
mocks.user.EXPECT().IsConnected().Return(true) mocks.user.EXPECT().IsConnected().Return(true)
mocks.user.EXPECT().IsCombinedAddressMode().Return(combinedMode) mocks.user.EXPECT().IsCombinedAddressMode().Return(combinedMode)
@ -102,20 +105,19 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unpar
}) })
mocks.client.EXPECT().ListLabels() mocks.client.EXPECT().ListLabels()
mocks.client.EXPECT().CountMessages("") mocks.client.EXPECT().CountMessages("")
mocks.client.EXPECT().GetEvent(gomock.Any()).
Return(&pmapi.Event{
EventID: "latestEventID",
}, nil).AnyTimes()
// We want to wait until first sync has finished. // Call to get latest event ID and then to process first event.
firstSyncWaiter := sync.WaitGroup{} mocks.client.EXPECT().GetEvent("").Return(&pmapi.Event{
firstSyncWaiter.Add(1) EventID: "firstEventID",
mocks.client.EXPECT(). }, nil)
ListMessages(gomock.Any()). mocks.client.EXPECT().GetEvent("firstEventID").Return(&pmapi.Event{
DoAndReturn(func(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error) { EventID: "latestEventID",
firstSyncWaiter.Done() }, nil)
return []*pmapi.Message{}, 0, nil
}) mocks.client.EXPECT().ListMessages(gomock.Any()).Return(msgs, len(msgs), nil).AnyTimes()
for _, msg := range msgs {
mocks.client.EXPECT().GetMessage(msg.ID).Return(msg, nil).AnyTimes()
}
var err error var err error
mocks.store, err = New( mocks.store, err = New(
@ -128,6 +130,16 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unpar
) )
require.NoError(mocks.tb, err) require.NoError(mocks.tb, err)
// Wait for sync to finish. // We want to wait until first sync has finished.
firstSyncWaiter.Wait() require.Eventually(mocks.tb, func() bool {
for _, msg := range msgs {
_, err := mocks.store.getMessageFromDB(msg.ID)
if err != nil {
// To see in test result the latest error for debugging.
fmt.Println("Sync wait error:", err)
return false
}
}
return true
}, 5*time.Second, 10*time.Millisecond)
} }

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) // Source: github.com/ProtonMail/proton-bridge/internal/transfer (interfaces: PanicHandler,ClientManager,IMAPClientProvider)
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
@ -8,6 +8,8 @@ import (
reflect "reflect" reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imap "github.com/emersion/go-imap"
sasl "github.com/emersion/go-sasl"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )
@ -96,3 +98,170 @@ func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Cal
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
} }
// MockIMAPClientProvider is a mock of IMAPClientProvider interface
type MockIMAPClientProvider struct {
ctrl *gomock.Controller
recorder *MockIMAPClientProviderMockRecorder
}
// MockIMAPClientProviderMockRecorder is the mock recorder for MockIMAPClientProvider
type MockIMAPClientProviderMockRecorder struct {
mock *MockIMAPClientProvider
}
// NewMockIMAPClientProvider creates a new mock instance
func NewMockIMAPClientProvider(ctrl *gomock.Controller) *MockIMAPClientProvider {
mock := &MockIMAPClientProvider{ctrl: ctrl}
mock.recorder = &MockIMAPClientProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockIMAPClientProvider) EXPECT() *MockIMAPClientProviderMockRecorder {
return m.recorder
}
// Authenticate mocks base method
func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Authenticate", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Authenticate indicates an expected call of Authenticate
func (mr *MockIMAPClientProviderMockRecorder) Authenticate(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockIMAPClientProvider)(nil).Authenticate), arg0)
}
// Capability mocks base method
func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Capability")
ret0, _ := ret[0].(map[string]bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Capability indicates an expected call of Capability
func (mr *MockIMAPClientProviderMockRecorder) Capability() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capability", reflect.TypeOf((*MockIMAPClientProvider)(nil).Capability))
}
// Fetch mocks base method
func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Fetch indicates an expected call of Fetch
func (mr *MockIMAPClientProviderMockRecorder) Fetch(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).Fetch), arg0, arg1, arg2)
}
// List mocks base method
func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.MailboxInfo) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// List indicates an expected call of List
func (mr *MockIMAPClientProviderMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIMAPClientProvider)(nil).List), arg0, arg1, arg2)
}
// Login mocks base method
func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Login", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Login indicates an expected call of Login
func (mr *MockIMAPClientProviderMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockIMAPClientProvider)(nil).Login), arg0, arg1)
}
// Select mocks base method
func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxStatus, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Select", arg0, arg1)
ret0, _ := ret[0].(*imap.MailboxStatus)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Select indicates an expected call of Select
func (mr *MockIMAPClientProviderMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockIMAPClientProvider)(nil).Select), arg0, arg1)
}
// State mocks base method
func (m *MockIMAPClientProvider) State() imap.ConnState {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "State")
ret0, _ := ret[0].(imap.ConnState)
return ret0
}
// State indicates an expected call of State
func (mr *MockIMAPClientProviderMockRecorder) State() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockIMAPClientProvider)(nil).State))
}
// Support mocks base method
func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Support", arg0)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Support indicates an expected call of Support
func (mr *MockIMAPClientProviderMockRecorder) Support(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Support", reflect.TypeOf((*MockIMAPClientProvider)(nil).Support), arg0)
}
// SupportAuth mocks base method
func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SupportAuth", arg0)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SupportAuth indicates an expected call of SupportAuth
func (mr *MockIMAPClientProviderMockRecorder) SupportAuth(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportAuth", reflect.TypeOf((*MockIMAPClientProvider)(nil).SupportAuth), arg0)
}
// UidFetch mocks base method
func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UidFetch", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// UidFetch indicates an expected call of UidFetch
func (mr *MockIMAPClientProviderMockRecorder) UidFetch(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UidFetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).UidFetch), arg0, arg1, arg2)
}

View File

@ -21,28 +21,49 @@ import (
"net" "net"
"strings" "strings"
imapClient "github.com/emersion/go-imap/client" "github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
) )
type IMAPClientProvider interface {
Capability() (map[string]bool, error)
Support(cap string) (bool, error)
State() imap.ConnState
SupportAuth(mech string) (bool, error)
Authenticate(auth sasl.Client) error
Login(username, password string) error
List(ref, name string, ch chan *imap.MailboxInfo) error
Select(name string, readOnly bool) (*imap.MailboxStatus, error)
Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error
UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error
}
// IMAPProvider implements export from IMAP server. // IMAPProvider implements export from IMAP server.
type IMAPProvider struct { type IMAPProvider struct {
username string username string
password string password string
addr string addr string
client *imapClient.Client clientDialer func(addr string) (IMAPClientProvider, error)
client IMAPClientProvider
timeIt *timeIt timeIt *timeIt
} }
// NewIMAPProvider returns new IMAPProvider. // NewIMAPProvider returns new IMAPProvider.
func NewIMAPProvider(username, password, host, port string) (*IMAPProvider, error) { func NewIMAPProvider(username, password, host, port string) (*IMAPProvider, error) {
return newIMAPProvider(imapClientDial, username, password, host, port)
}
func newIMAPProvider(clientDialer func(string) (IMAPClientProvider, error), username, password, host, port string) (*IMAPProvider, error) {
p := &IMAPProvider{ p := &IMAPProvider{
username: username, username: username,
password: password, password: password,
addr: net.JoinHostPort(host, port), addr: net.JoinHostPort(host, port),
timeIt: newTimeIt("imap"), timeIt: newTimeIt("imap"),
clientDialer: clientDialer,
} }
if err := p.auth(); err != nil { if err := p.auth(); err != nil {

View File

@ -84,12 +84,37 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
p.timeIt.start("load", rule.SourceMailbox.Name) p.timeIt.start("load", rule.SourceMailbox.Name)
defer p.timeIt.stop("load", rule.SourceMailbox.Name) defer p.timeIt.stop("load", rule.SourceMailbox.Name)
log := log.WithField("mailbox", rule.SourceMailbox.Name)
messagesInfo := map[string]imapMessageInfo{} messagesInfo := map[string]imapMessageInfo{}
fetchItems := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size}
if rule.HasTimeLimit() {
fetchItems = append(fetchItems, imap.FetchEnvelope)
}
processMessageCallback := func(imapMessage *imap.Message) {
if rule.HasTimeLimit() {
t := imapMessage.Envelope.Date.Unix()
if t != 0 && !rule.isTimeInRange(t) {
log.WithField("uid", imapMessage.Uid).Debug("Message skipped due to time")
return
}
}
id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
// We use ID as key to ensure we have every unique message only once.
// Some IMAP servers responded twice the same message...
messagesInfo[id] = imapMessageInfo{
id: id,
uid: imapMessage.Uid,
size: imapMessage.Size,
}
progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
}
pageStart := uint32(1) pageStart := uint32(1)
pageEnd := imapPageSize pageEnd := imapPageSize
for { for {
if progress.shouldStop() { if progress.shouldStop() || pageStart > count {
break break
} }
@ -100,45 +125,21 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
seqSet := &imap.SeqSet{} seqSet := &imap.SeqSet{}
seqSet.AddRange(pageStart, pageEnd) seqSet.AddRange(pageStart, pageEnd)
err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback)
items := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size} if err != nil {
if rule.HasTimeLimit() { log.WithError(err).WithField("idx", seqSet).Warning("Load batch fetch failed, trying one by one")
items = append(items, imap.FetchEnvelope) for ; pageStart <= pageEnd; pageStart++ {
} seqSet := &imap.SeqSet{}
seqSet.AddNum(pageStart)
pageMsgCount := uint32(0) if err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback); err != nil {
processMessageCallback := func(imapMessage *imap.Message) { log.WithError(err).WithField("idx", seqSet).Warning("Load fetch failed, skipping the message")
pageMsgCount++
if rule.HasTimeLimit() {
t := imapMessage.Envelope.Date.Unix()
if t != 0 && !rule.isTimeInRange(t) {
log.WithField("uid", imapMessage.Uid).Debug("Message skipped due to time")
return
} }
} }
id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
// We use ID as key to ensure we have every unique message only once.
// Some IMAP servers responded twice the same message...
messagesInfo[id] = imapMessageInfo{
id: id,
uid: imapMessage.Uid,
size: imapMessage.Size,
}
progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
} }
progress.callWrap(func() error { pageStart = pageEnd + 1
return p.fetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
})
if pageMsgCount < imapPageSize {
break
}
pageStart = pageEnd
pageEnd += imapPageSize pageEnd += imapPageSize
} }
return messagesInfo return messagesInfo
} }

View File

@ -0,0 +1,100 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package transfer
import (
"fmt"
"testing"
"github.com/emersion/go-imap"
gomock "github.com/golang/mock/gomock"
"github.com/pkg/errors"
r "github.com/stretchr/testify/require"
)
func newTestIMAPProvider(t *testing.T, m mocks) *IMAPProvider {
m.imapClientProvider.EXPECT().State().Return(imap.ConnectedState).AnyTimes()
m.imapClientProvider.EXPECT().Capability().Return(map[string]bool{
"AUTH": true,
}, nil).AnyTimes()
dialer := func(string) (IMAPClientProvider, error) {
return m.imapClientProvider, nil
}
provider, err := newIMAPProvider(dialer, "user", "pass", "host", "42")
r.NoError(t, err)
return provider
}
func TestProviderIMAPLoadMessagesInfo(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
provider := newTestIMAPProvider(t, m)
progress := newProgress(log, nil)
drainProgressUpdateChannel(&progress)
rule := &Rule{SourceMailbox: Mailbox{Name: "Mailbox"}}
uidValidity := 1
count := 2200
failingIndex := 2100
m.imapClientProvider.EXPECT().Select(rule.SourceMailbox.Name, gomock.Any()).Return(&imap.MailboxStatus{}, nil).AnyTimes()
m.imapClientProvider.EXPECT().
Fetch(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(seqSet *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
defer close(ch)
for _, seq := range seqSet.Set {
for i := seq.Start; i <= seq.Stop; i++ {
if int(i) == failingIndex {
return errors.New("internal server error")
}
ch <- &imap.Message{
SeqNum: i,
Uid: i * 10,
Size: i * 100,
}
}
}
return nil
}).
// 2200 messages is split into two batches (2000 and 200),
// the second one fails and makes 200 calls (one-by-one).
// Plus two failed requests are repeated `imapRetries` times.
Times(2 + 200 + (2 * (imapRetries - 1)))
messageInfo := provider.loadMessagesInfo(rule, &progress, uint32(uidValidity), uint32(count))
r.Equal(t, count-1, len(messageInfo)) // One message produces internal server error.
for index := 1; index <= count; index++ {
uid := index * 10
key := fmt.Sprintf("%s_%d:%d", rule.SourceMailbox.Name, uidValidity, uid)
if index == failingIndex {
r.Empty(t, messageInfo[key])
continue
}
r.Equal(t, imapMessageInfo{
id: key,
uid: uint32(uid),
size: uint32(index * 100),
}, messageInfo[key])
}
}

View File

@ -24,10 +24,11 @@ import (
"time" "time"
imapID "github.com/ProtonMail/go-imap-id" imapID "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "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"
sasl "github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -51,6 +52,43 @@ func (l *imapErrorLogger) Println(v ...interface{}) {
l.log.Errorln(v...) l.log.Errorln(v...)
} }
func imapClientDial(addr string) (IMAPClientProvider, error) {
if _, err := net.DialTimeout("tcp", addr, imapDialTimeout); err != nil {
return nil, errors.Wrap(err, "failed to dial server")
}
client, err := imapClientDialHelper(addr)
if err == nil {
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
// Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
// Also, this spams a lot, uncomment once needed during development.
//client.SetDebug(imap.NewDebugWriter(
// logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
// logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
//))
}
return client, err
}
func imapClientDialHelper(addr string) (*imapClient.Client, error) {
host, _, _ := net.SplitHostPort(addr)
if host == "127.0.0.1" {
return imapClient.Dial(addr)
}
// IMAP mail.yahoo.com has problem with golang TLS 1.3 implementation
// with weird behaviour, i.e., Yahoo does not return error during dial
// or handshake but server does logs out right after successful login
// leaving no time to perform any action.
// Limiting TLS to version 1.2 is working just fine.
var tlsConf *tls.Config
if strings.Contains(strings.ToLower(host), "yahoo") {
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
}
return imapClient.DialTLS(addr, tlsConf)
}
func (p *IMAPProvider) ensureConnection(callback func() error) error { func (p *IMAPProvider) ensureConnection(callback func() error) error {
return p.ensureConnectionAndSelection(callback, "") return p.ensureConnectionAndSelection(callback, "")
} }
@ -138,41 +176,10 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
log.Info("Connecting to server") log.Info("Connecting to server")
if _, err := net.DialTimeout("tcp", p.addr, imapDialTimeout); err != nil { client, err := p.clientDialer(p.addr)
return ErrIMAPConnection{imapError{Err: err, Message: "failed to dial server"}}
}
var client *imapClient.Client
var err error
host, _, _ := net.SplitHostPort(p.addr)
if host == "127.0.0.1" {
client, err = imapClient.Dial(p.addr)
} else {
// IMAP.mail.yahoo.com have problem with golang TLS1.3
// implementation with weird behaviour i.e. Yahoo
// no error during dial or handshake but server logs out right
// after successful login leaving no time to perform any
// action. It was discovered that limiting to maximum TLS
// version 1.2 for yahoo servers is working solution.
var tlsConf *tls.Config
if strings.Contains(strings.ToLower(host), "yahoo") {
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
}
client, err = imapClient.DialTLS(p.addr, tlsConf)
}
if err != nil { if err != nil {
return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}} return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}}
} }
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
// Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
// Also, this spams a lot, uncomment once needed during development.
//client.SetDebug(imap.NewDebugWriter(
// logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
// logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
//))
p.client = client p.client = client
log.Info("Connected") log.Info("Connected")
@ -210,13 +217,15 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
log.Info("Logged in") log.Info("Logged in")
idClient := imapID.NewClient(p.client) if c, ok := p.client.(*imapClient.Client); ok {
if ok, err := idClient.SupportID(); err == nil && ok { idClient := imapID.NewClient(c)
serverID, err := idClient.ID(imapID.ID{ if ok, err := idClient.SupportID(); err == nil && ok {
imapID.FieldName: "ImportExport", serverID, err := idClient.ID(imapID.ID{
imapID.FieldVersion: "beta", imapID.FieldName: "ImportExport",
}) imapID.FieldVersion: constants.Version,
log.WithField("ID", serverID).WithError(err).Debug("Server info") })
log.WithField("ID", serverID).WithError(err).Debug("Server info")
}
} }
return err return err

View File

@ -124,6 +124,12 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
return "", errors.Wrap(err, "failed to parse message") return "", errors.Wrap(err, "failed to parse message")
} }
// Trying to encrypt an encrypted draft will return an error;
// users are forbidden to import messages encrypted with foreign keys to drafts.
if message.IsEncrypted() {
return "", errors.New("refusing to import draft encrypted by foreign key")
}
p.timeIt.start("encrypt", msg.ID) p.timeIt.start("encrypt", msg.ID)
err = message.Encrypt(p.keyRing, nil) err = message.Encrypt(p.keyRing, nil)
p.timeIt.stop("encrypt", msg.ID) p.timeIt.stop("encrypt", msg.ID)
@ -171,13 +177,12 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
} }
func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress, msg Message, preparedImportRequestsCh chan map[string]*pmapi.ImportMsgReq) { func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress, msg Message, preparedImportRequestsCh chan map[string]*pmapi.ImportMsgReq) {
importMsgReq, err := p.generateImportMsgReq(msg, rules.globalMailbox) importMsgReq, err := p.generateImportMsgReq(rules, progress, msg)
if err != nil { if err != nil {
progress.messageImported(msg.ID, "", err) progress.messageImported(msg.ID, "", err)
return return
} }
if importMsgReq == nil || progress.shouldStop() {
if progress.shouldStop() {
return return
} }
@ -191,17 +196,26 @@ func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress,
p.nextImportRequestsSize += importMsgReqSize p.nextImportRequestsSize += importMsgReqSize
} }
func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox) (*pmapi.ImportMsgReq, error) { func (p *PMAPIProvider) generateImportMsgReq(rules transferRules, progress *Progress, msg Message) (*pmapi.ImportMsgReq, error) {
message, attachmentReaders, err := p.parseMessage(msg) message, attachmentReaders, err := p.parseMessage(msg)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to parse message") return nil, errors.Wrap(err, "failed to parse message")
} }
p.timeIt.start("encrypt", msg.ID) var body []byte
body, err := p.encryptMessage(message, attachmentReaders) if message.IsEncrypted() {
p.timeIt.stop("encrypt", msg.ID) if rules.skipEncryptedMessages {
if err != nil { progress.messageSkipped(msg.ID)
return nil, errors.Wrap(err, "failed to encrypt message") return nil, nil
}
body = msg.Body
} else {
p.timeIt.start("encrypt", msg.ID)
body, err = p.encryptMessage(message, attachmentReaders)
p.timeIt.stop("encrypt", msg.ID)
if err != nil {
return nil, errors.Wrap(err, "failed to encrypt message")
}
} }
unread := 0 unread := 0
@ -216,8 +230,8 @@ func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox
labelIDs = append(labelIDs, target.ID) labelIDs = append(labelIDs, target.ID)
} }
} }
if globalMailbox != nil { if rules.globalMailbox != nil {
labelIDs = append(labelIDs, globalMailbox.ID) labelIDs = append(labelIDs, rules.globalMailbox.ID)
} }
return &pmapi.ImportMsgReq{ return &pmapi.ImportMsgReq{

View File

@ -85,7 +85,7 @@ func testTransferFrom(t *testing.T, rules transferRules, provider TargetProvider
progress.finish() progress.finish()
}() }()
maxWait := time.Duration(len(messages)) * time.Second maxWait := time.Duration(len(messages)) * 2 * time.Second
a.Eventually(t, func() bool { a.Eventually(t, func() bool {
return progress.updateCh == nil return progress.updateCh == nil
}, maxWait, 10*time.Millisecond, "Waiting for imported messages timed out") }, maxWait, 10*time.Millisecond, "Waiting for imported messages timed out")

View File

@ -49,7 +49,7 @@ type transferRules struct {
globalToTime int64 globalToTime int64
// skipEncryptedMessages determines whether message which cannot // skipEncryptedMessages determines whether message which cannot
// be decrypted should be exported or skipped. // be decrypted should be imported/exported or skipped.
skipEncryptedMessages bool skipEncryptedMessages bool
} }

View File

@ -90,7 +90,7 @@ func (t *Transfer) setDefaultRules() error {
} }
// SetSkipEncryptedMessages sets whether message which cannot be decrypted // SetSkipEncryptedMessages sets whether message which cannot be decrypted
// should be exported or skipped. // should be imported/exported or skipped.
func (t *Transfer) SetSkipEncryptedMessages(skip bool) { func (t *Transfer) SetSkipEncryptedMessages(skip bool) {
t.rules.setSkipEncryptedMessages(skip) t.rules.setSkipEncryptedMessages(skip)
} }

View File

@ -31,11 +31,12 @@ import (
type mocks struct { type mocks struct {
t *testing.T t *testing.T
ctrl *gomock.Controller ctrl *gomock.Controller
panicHandler *transfermocks.MockPanicHandler panicHandler *transfermocks.MockPanicHandler
clientManager *transfermocks.MockClientManager clientManager *transfermocks.MockClientManager
pmapiClient *pmapimocks.MockClient imapClientProvider *transfermocks.MockIMAPClientProvider
pmapiConfig *pmapi.ClientConfig pmapiClient *pmapimocks.MockClient
pmapiConfig *pmapi.ClientConfig
keyring *crypto.KeyRing keyring *crypto.KeyRing
} }
@ -46,12 +47,13 @@ func initMocks(t *testing.T) mocks {
m := mocks{ m := mocks{
t: t, t: t,
ctrl: mockCtrl, ctrl: mockCtrl,
panicHandler: transfermocks.NewMockPanicHandler(mockCtrl), panicHandler: transfermocks.NewMockPanicHandler(mockCtrl),
clientManager: transfermocks.NewMockClientManager(mockCtrl), clientManager: transfermocks.NewMockClientManager(mockCtrl),
pmapiClient: pmapimocks.NewMockClient(mockCtrl), imapClientProvider: transfermocks.NewMockIMAPClientProvider(mockCtrl),
pmapiConfig: &pmapi.ClientConfig{}, pmapiClient: pmapimocks.NewMockClient(mockCtrl),
keyring: newTestKeyring(), pmapiConfig: &pmapi.ClientConfig{},
keyring: newTestKeyring(),
} }
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).AnyTimes() m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).AnyTimes()

View File

@ -27,7 +27,6 @@ import (
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -43,8 +42,6 @@ type User struct {
clientManager ClientManager clientManager ClientManager
credStorer CredentialsStorer credStorer CredentialsStorer
imapUpdatesChannel chan imapBackend.Update
storeFactory StoreMaker storeFactory StoreMaker
store *store.Store store *store.Store
@ -95,7 +92,7 @@ func (u *User) client() pmapi.Client {
// have the apitoken and password), authorising the user against the api, loading the user store (creating a new one // have the apitoken and password), authorising the user against the api, loading the user store (creating a new one
// if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if // if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if
// something in the store changed). // something in the store changed).
func (u *User) init(idleUpdates chan imapBackend.Update) (err error) { func (u *User) init() (err error) {
u.log.Info("Initialising user") u.log.Info("Initialising user")
// Reload the user's credentials (if they log out and back in we need the new // Reload the user's credentials (if they log out and back in we need the new
@ -134,20 +131,9 @@ func (u *User) init(idleUpdates chan imapBackend.Update) (err error) {
} }
u.store = store u.store = store
// Save the imap updates channel here so it can be set later when imap connects.
u.imapUpdatesChannel = idleUpdates
return err return err
} }
func (u *User) SetIMAPIdleUpdateChannel() {
if u.store == nil {
return
}
u.store.SetIMAPUpdateChannel(u.imapUpdatesChannel)
}
// authorizeIfNecessary checks whether user is logged in and is connected to api auth channel. // authorizeIfNecessary checks whether user is logged in and is connected to api auth channel.
// If user is not already connected to the api auth channel (for example there was no internet during start), // If user is not already connected to the api auth channel (for example there was no internet during start),
// it tries to connect it. // it tries to connect it.
@ -539,7 +525,7 @@ func (u *User) CloseAllConnections() {
} }
if u.store != nil { if u.store != nil {
u.store.SetIMAPUpdateChannel(nil) u.store.SetChangeNotifier(nil)
} }
} }

View File

@ -221,7 +221,7 @@ func TestCheckBridgeLoginLoggedOut(t *testing.T) {
m.pmapiClient.EXPECT().Addresses().Return(nil), m.pmapiClient.EXPECT().Addresses().Return(nil),
) )
err = user.init(nil) err = user.init()
assert.Error(t, err) assert.Error(t, err)
defer cleanUpUserData(user) defer cleanUpUserData(user)

View File

@ -139,7 +139,7 @@ func checkNewUserHasCredentials(creds *credentials.Credentials, m mocks) {
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker) user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
defer cleanUpUserData(user) defer cleanUpUserData(user)
_ = user.init(nil) _ = user.init()
waitForEvents() waitForEvents()

View File

@ -20,8 +20,6 @@ package users
import ( import (
"testing" "testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -31,17 +29,12 @@ func testNewUser(m mocks) *User {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
mockConnectedUser(m) mockConnectedUser(m)
mockEventLoopNoAction(m)
gomock.InOrder(
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).MaxTimes(1),
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).MaxTimes(1),
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1),
)
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker) user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
assert.NoError(m.t, err) assert.NoError(m.t, err)
err = user.init(nil) err = user.init()
assert.NoError(m.t, err) assert.NoError(m.t, err)
mockAuthUpdate(user, "reftok", m) mockAuthUpdate(user, "reftok", m)
@ -53,17 +46,12 @@ func testNewUserForLogout(m mocks) *User {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
mockConnectedUser(m) mockConnectedUser(m)
mockEventLoopNoAction(m)
gomock.InOrder(
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).MaxTimes(1),
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).MaxTimes(1),
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1),
)
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker) user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
assert.NoError(m.t, err) assert.NoError(m.t, err)
err = user.init(nil) err = user.init()
assert.NoError(m.t, err) assert.NoError(m.t, err)
return user return user

View File

@ -26,7 +26,6 @@ import (
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/pkg/errors" "github.com/pkg/errors"
logrus "github.com/sirupsen/logrus" logrus "github.com/sirupsen/logrus"
@ -58,11 +57,6 @@ type Users struct {
// as is, without requesting server again. // as is, without requesting server again.
useOnlyActiveAddresses bool useOnlyActiveAddresses bool
// idleUpdates is a channel which the imap backend listens to and which it
// uses to send idle updates to the mail client (eg thunderbird).
// The user stores should send idle updates on this channel.
idleUpdates chan imapBackend.Update
lock sync.RWMutex lock sync.RWMutex
// stopAll can be closed to stop all goroutines from looping (watchAppOutdated, watchAPIAuths, heartbeat etc). // stopAll can be closed to stop all goroutines from looping (watchAppOutdated, watchAPIAuths, heartbeat etc).
@ -88,7 +82,6 @@ func New(
credStorer: credStorer, credStorer: credStorer,
storeFactory: storeFactory, storeFactory: storeFactory,
useOnlyActiveAddresses: useOnlyActiveAddresses, useOnlyActiveAddresses: useOnlyActiveAddresses,
idleUpdates: make(chan imapBackend.Update),
lock: sync.RWMutex{}, lock: sync.RWMutex{},
stopAll: make(chan struct{}), stopAll: make(chan struct{}),
} }
@ -132,7 +125,7 @@ func (u *Users) loadUsersFromCredentialsStore() (err error) {
u.users = append(u.users, user) u.users = append(u.users, user)
if initUserErr := user.init(u.idleUpdates); initUserErr != nil { if initUserErr := user.init(); initUserErr != nil {
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user") l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
} }
} }
@ -285,7 +278,7 @@ func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassphra
return errors.Wrap(err, "failed to update token of user in credentials store") return errors.Wrap(err, "failed to update token of user in credentials store")
} }
if err = user.init(u.idleUpdates); err != nil { if err = user.init(); err != nil {
return errors.Wrap(err, "failed to initialise user") return errors.Wrap(err, "failed to initialise user")
} }
@ -326,7 +319,7 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassphra
// The user needs to be part of the users list in order for it to receive an auth during initialisation. // The user needs to be part of the users list in order for it to receive an auth during initialisation.
u.users = append(u.users, user) u.users = append(u.users, user)
if err = user.init(u.idleUpdates); err != nil { if err = user.init(); err != nil {
u.users = u.users[:len(u.users)-1] u.users = u.users[:len(u.users)-1]
return errors.Wrap(err, "failed to initialise user") return errors.Wrap(err, "failed to initialise user")
} }
@ -464,15 +457,6 @@ func (u *Users) SendMetric(m metrics.Metric) {
}).Debug("Metric successfully sent") }).Debug("Metric successfully sent")
} }
// GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
func (u *Users) GetIMAPUpdatesChannel() chan imapBackend.Update {
if u.idleUpdates == nil {
log.Warn("IMAP updates channel is nil")
}
return u.idleUpdates
}
// AllowProxy instructs the app to use DoH to access an API proxy if necessary. // AllowProxy instructs the app to use DoH to access an API proxy if necessary.
// It also needs to work before the app is initialised (because we may need to use the proxy at startup). // It also needs to work before the app is initialised (because we may need to use the proxy at startup).
func (u *Users) AllowProxy() { func (u *Users) AllowProxy() {

View File

@ -23,7 +23,6 @@ import (
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -86,36 +85,6 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected}) checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
} }
func mockConnectedUser(m mocks) {
gomock.InOrder(
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
// Set up mocks for store initialisation for the authorized user.
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
)
}
// mockAuthUpdate simulates users calling UpdateAuthToken on the given user.
// This would normally be done by users when it receives an auth from the ClientManager,
// but as we don't have a full users instance here, we do this manually.
func mockAuthUpdate(user *User, token string, m mocks) {
gomock.InOrder(
m.credentialsStore.EXPECT().UpdateToken("user", ":"+token).Return(nil),
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(token), nil),
)
user.updateAuthToken(refreshWithToken(token))
waitForEvents()
}
func TestNewUsersWithConnectedUser(t *testing.T) { func TestNewUsersWithConnectedUser(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()

View File

@ -284,10 +284,37 @@ func TestClearData(t *testing.T) {
func mockEventLoopNoAction(m mocks) { func mockEventLoopNoAction(m mocks) {
// Set up mocks for starting the store's event loop (in store.New). // Set up mocks for starting the store's event loop (in store.New).
// The event loop runs in another goroutine so this might happen at any time. // The event loop runs in another goroutine so this might happen at any time.
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
}
func mockConnectedUser(m mocks) {
gomock.InOrder( gomock.InOrder(
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil), m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil),
// Set up mocks for performing the initial store sync. m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil),
m.pmapiClient.EXPECT().Unlock([]byte(testCredentials.MailboxPassword)).Return(nil),
// Set up mocks for store initialisation for the authorized user.
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil),
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
) )
} }
// mockAuthUpdate simulates users calling UpdateAuthToken on the given user.
// This would normally be done by users when it receives an auth from the ClientManager,
// but as we don't have a full users instance here, we do this manually.
func mockAuthUpdate(user *User, token string, m mocks) {
gomock.InOrder(
m.credentialsStore.EXPECT().UpdateToken("user", ":"+token).Return(nil),
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(token), nil),
)
user.updateAuthToken(refreshWithToken(token))
waitForEvents()
}

View File

@ -21,6 +21,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"github.com/ProtonMail/go-appdir" "github.com/ProtonMail/go-appdir"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
@ -188,6 +189,61 @@ func (c *Config) GetLogPrefix() string {
return "v" + c.version + "_" + c.revision return "v" + c.version + "_" + c.revision
} }
// GetLicenseFilePath returns path to liense file.
func (c *Config) GetLicenseFilePath() string {
path := c.getLicenseFilePath()
log.WithField("path", path).Info("License file path")
return path
}
func (c *Config) getLicenseFilePath() string {
// User can install app to different location, or user can run it
// directly from the package without installation, or it could be
// automatically updated (app started from differenet location).
// For all those cases, first let's check LICENSE next to the binary.
path := filepath.Join(filepath.Dir(os.Args[0]), "LICENSE")
if _, err := os.Stat(path); err == nil {
return path
}
switch runtime.GOOS {
case "linux":
appName := c.appName
if c.appName == "importExport" {
appName = "import-export"
}
// Most Linux distributions.
path := "/usr/share/doc/protonmail/" + appName + "/LICENSE"
if _, err := os.Stat(path); err == nil {
return path
}
// Arch distributions.
return "/usr/share/licenses/protonmail-" + appName + "/LICENSE"
case "darwin": //nolint[goconst]
path := filepath.Join(filepath.Dir(os.Args[0]), "..", "Resources", "LICENSE")
if _, err := os.Stat(path); err == nil {
return path
}
appName := "ProtonMail Bridge.app"
if c.appName == "importExport" {
appName = "ProtonMail Import-Export.app"
}
return "/Applications/" + appName + "/Contents/Resources/LICENSE"
case "windows":
path := filepath.Join(filepath.Dir(os.Args[0]), "LICENSE.txt")
if _, err := os.Stat(path); err == nil {
return path
}
// This should not happen, Windows should be handled by relative
// location to the binary above. This is just fallback which may
// or may not work, depends where user installed the app and how
// user started the app.
return filepath.FromSlash("C:/Program Files/Proton Technologies AG/ProtonMail Bridge/LICENSE.txt")
}
return ""
}
// GetTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP and API). // GetTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP and API).
func (c *Config) GetTLSCertPath() string { func (c *Config) GetTLSCertPath() string {
return filepath.Join(c.appDirs.UserConfig(), "cert.pem") return filepath.Join(c.appDirs.UserConfig(), "cert.pem")
@ -247,3 +303,17 @@ func (c *Config) GetDefaultIMAPPort() int {
func (c *Config) GetDefaultSMTPPort() int { func (c *Config) GetDefaultSMTPPort() int {
return 1025 return 1025
} }
// getAPIOS returns actual operating system.
func (c *Config) getAPIOS() string {
switch os := runtime.GOOS; os {
case "darwin": // nolint: goconst
return "macOS"
case "linux":
return "Linux"
case "windows":
return "Windows"
}
return "Linux"
}

View File

@ -57,6 +57,8 @@ var logCrashRgx = regexp.MustCompile("^v.*_crash_.*\\.log$") //nolint[gochecknog
// HandlePanic reports the crash to sentry or local file when sentry fails. // HandlePanic reports the crash to sentry or local file when sentry fails.
func HandlePanic(cfg *Config, output string) { func HandlePanic(cfg *Config, output string) {
sentry.SkipDuringUnwind()
if !cfg.IsDevMode() { if !cfg.IsDevMode() {
apiCfg := cfg.GetAPIConfig() apiCfg := cfg.GetAPIConfig()
if err := sentry.ReportSentryCrash(apiCfg.ClientID, apiCfg.AppVersion, apiCfg.UserAgent, errors.New(output)); err != nil { if err := sentry.ReportSentryCrash(apiCfg.ClientID, apiCfg.AppVersion, apiCfg.UserAgent, errors.New(output)); err != nil {

View File

@ -29,7 +29,7 @@ import (
func (c *Config) GetAPIConfig() *pmapi.ClientConfig { func (c *Config) GetAPIConfig() *pmapi.ClientConfig {
return &pmapi.ClientConfig{ return &pmapi.ClientConfig{
AppVersion: strings.Title(c.appName) + "_" + c.version, AppVersion: c.getAPIOS() + strings.Title(c.appName) + "_" + c.version,
ClientID: c.appName, ClientID: c.appName,
} }
} }

View File

@ -31,7 +31,7 @@ import (
func (c *Config) GetAPIConfig() *pmapi.ClientConfig { func (c *Config) GetAPIConfig() *pmapi.ClientConfig {
return &pmapi.ClientConfig{ return &pmapi.ClientConfig{
AppVersion: strings.Title(c.appName) + "_" + c.version, AppVersion: c.getAPIOS() + strings.Title(c.appName) + "_" + c.version,
ClientID: c.appName, ClientID: c.appName,
Timeout: 25 * time.Minute, // Overall request timeout (~25MB / 25 mins => ~16kB/s, should be reasonable). Timeout: 25 * time.Minute, // Overall request timeout (~25MB / 25 mins => ~16kB/s, should be reasonable).
FirstReadTimeout: 30 * time.Second, // 30s to match 30s response header timeout. FirstReadTimeout: 30 * time.Second, // 30s to match 30s response header timeout.

View File

@ -45,6 +45,11 @@ func Parse(r io.Reader, key, keyName string) (m *pmapi.Message, mimeBody, plainB
return return
} }
if err = convertEncodedTransferEncoding(p); err != nil {
err = errors.Wrap(err, "failed to convert encoded transfer encodings")
return
}
if err = convertForeignEncodings(p); err != nil { if err = convertForeignEncodings(p); err != nil {
err = errors.Wrap(err, "failed to convert foreign encodings") err = errors.Wrap(err, "failed to convert foreign encodings")
return return
@ -89,6 +94,30 @@ func Parse(r io.Reader, key, keyName string) (m *pmapi.Message, mimeBody, plainB
return m, mimeBodyBuffer.String(), plainBody, attReaders, nil return m, mimeBodyBuffer.String(), plainBody, attReaders, nil
} }
// convertEncodedTransferEncoding decodes any RFC2047-encoded content transfer encodings.
// Such content transfer encodings go against RFC but still exist in the wild anyway.
func convertEncodedTransferEncoding(p *parser.Parser) error {
logrus.Trace("Converting encoded transfer encoding")
return p.NewWalker().
RegisterDefaultHandler(func(p *parser.Part) error {
encoding := p.Header.Get("Content-Transfer-Encoding")
if encoding == "" {
return nil
}
dec, err := pmmime.WordDec.DecodeHeader(encoding)
if err != nil {
return err
}
p.Header.Set("Content-Transfer-Encoding", dec)
return nil
}).
Walk()
}
func convertForeignEncodings(p *parser.Parser) error { func convertForeignEncodings(p *parser.Parser) error {
logrus.Trace("Converting foreign encodings") logrus.Trace("Converting foreign encodings")
@ -104,12 +133,11 @@ func convertForeignEncodings(p *parser.Parser) error {
return p.ConvertToUTF8() return p.ConvertToUTF8()
}). }).
RegisterDefaultHandler(func(p *parser.Part) error { RegisterDefaultHandler(func(p *parser.Part) error {
t, params, _ := p.ContentType()
// multipart/alternative, for example, can contain extra charset. // multipart/alternative, for example, can contain extra charset.
if params != nil && params["charset"] != "" { if _, params, _ := p.ContentType(); params != nil && params["charset"] != "" {
return p.ConvertToUTF8() return p.ConvertToUTF8()
} }
logrus.WithField("type", t).Trace("Not converting part to utf-8")
return nil return nil
}). }).
Walk() Walk()

View File

@ -22,6 +22,7 @@ import (
"io/ioutil" "io/ioutil"
"github.com/emersion/go-message" "github.com/emersion/go-message"
"github.com/sirupsen/logrus"
) )
type Parser struct { type Parser struct {
@ -33,8 +34,15 @@ func New(r io.Reader) (*Parser, error) {
p := new(Parser) p := new(Parser)
entity, err := message.Read(newEndOfMailTrimmer(r)) entity, err := message.Read(newEndOfMailTrimmer(r))
if err != nil && !message.IsUnknownCharset(err) { if err != nil {
return nil, err switch {
case message.IsUnknownCharset(err):
logrus.WithError(err).Warning("Message has an unknown charset")
case message.IsUnknownEncoding(err):
logrus.WithError(err).Warning("Message has an unknown encoding")
default:
return nil, err
}
} }
if err := p.parseEntity(entity); err != nil { if err := p.parseEntity(entity); err != nil {

View File

@ -480,6 +480,37 @@ func TestParseWithTrailingEndOfMailIndicator(t *testing.T) {
assert.Equal(t, "boo!", plainBody) assert.Equal(t, "boo!", plainBody)
} }
func TestParseEncodedContentType(t *testing.T) {
f := getFileReader("rfc2047-content-transfer-encoding.eml")
m, _, plainBody, _, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@sender.com>`, m.Sender.String())
assert.Equal(t, `<user@somewhere.org>`, m.ToList[0].String())
assert.Equal(t, "bodybodybody\n", plainBody)
}
func TestParseNonEncodedContentType(t *testing.T) {
f := getFileReader("non-encoded-content-transfer-encoding.eml")
m, _, plainBody, _, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@sender.com>`, m.Sender.String())
assert.Equal(t, `<user@somewhere.org>`, m.ToList[0].String())
assert.Equal(t, "bodybodybody\n", plainBody)
}
func TestParseEncodedContentTypeBad(t *testing.T) {
f := getFileReader("rfc2047-content-transfer-encoding-bad.eml")
_, _, _, _, err := Parse(f, "", "") // nolint[dogsled]
require.Error(t, err)
}
func getFileReader(filename string) io.Reader { func getFileReader(filename string) io.Reader {
f, err := os.Open(filepath.Join("testdata", filename)) f, err := os.Open(filepath.Join("testdata", filename))
if err != nil { if err != nil {

View File

@ -0,0 +1,10 @@
To: user@somewhere.org
Subject: =?utf-8?Q?aoeuaoeuaoeu?=
Date: Sat, 16 Jun 2020 17:36:02 +0200
MIME-Version: 1.0
Content-Type: text/plain;
charset="utf-8"
Content-Transfer-Encoding: 8bit
From: =?utf-8?Q?Sender?= <sender@sender.com>
bodybodybody

View File

@ -0,0 +1,10 @@
To: user@somewhere.org
Subject: =?utf-8?Q?aoeuaoeuaoeu?=
Date: Sat, 16 Jun 2020 17:36:02 +0200
MIME-Version: 1.0
Content-Type: text/plain;
charset="utf-8"
Content-Transfer-Encoding: =?utf-8?Q?8bit
From: =?utf-8?Q?Sender?= <sender@sender.com>
bodybodybody

View File

@ -0,0 +1,10 @@
To: user@somewhere.org
Subject: =?utf-8?Q?aoeuaoeuaoeu?=
Date: Sat, 16 Jun 2020 17:36:02 +0200
MIME-Version: 1.0
Content-Type: text/plain;
charset="utf-8"
Content-Transfer-Encoding: =?utf-8?Q?8bit?=
From: =?utf-8?Q?Sender?= <sender@sender.com>
bodybodybody

View File

@ -43,7 +43,7 @@ func startServer() {
_, _ = w.Write([]byte("OK")) _, _ = w.Write([]byte("OK"))
}) })
http.HandleFunc("/timeout", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/timeout", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second) time.Sleep(testRequestTimeout + time.Second) // Add extra second to be sure it will timeout.
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK")) _, _ = w.Write([]byte("OK"))
}) })

View File

@ -254,7 +254,7 @@ func (c *client) doBuffered(req *http.Request, bodyBuffer []byte, retryUnauthori
head += "\n" head += "\n"
} }
c.log.Tracef("REQHEAD \n%s", head) c.log.Tracef("REQHEAD \n%s", head)
c.log.Tracef("REQBODY '%s'", string(bodyBuffer)) c.log.Tracef("REQBODY '%s'", printBytes(bodyBuffer))
} }
hasBody := len(bodyBuffer) > 0 hasBody := len(bodyBuffer) > 0

43
pkg/pmapi/debug.go Normal file
View File

@ -0,0 +1,43 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package pmapi
import "unicode/utf8"
func printBytes(body []byte) string {
if utf8.Valid(body) {
return string(body)
}
enc := []rune{}
for _, b := range body {
switch {
case b == 9:
enc = append(enc, rune('⟼'))
case b == 13:
enc = append(enc, rune('↵'))
case b < 32, b == 127:
enc = append(enc, '◡')
case b > 31 && b < 127, b == 10:
enc = append(enc, rune(b))
default:
enc = append(enc, 9728+rune(b))
}
}
return string(enc)
}

View File

@ -67,6 +67,14 @@ func (req *ImportReq) WriteTo(w *multipart.Writer) (err error) {
if _, err = fw.Write(msg.Body); err != nil { if _, err = fw.Write(msg.Body); err != nil {
return return
} }
// Adding new line to properly fetch the whole body on the API side.
// The reason is the bug in PHP: https://bugs.php.net/bug.php?id=75923
// Messages generated by PM already have it but importing already
// encrypted messages might not have it.
if _, err = fw.Write([]byte("\r\n")); err != nil {
return
}
} }
return err return err

View File

@ -127,7 +127,7 @@ func TestClient_Import(t *testing.T) { // nolint[funlen]
t.Error("Expected no error while reading second part body, got:", err) t.Error("Expected no error while reading second part body, got:", err)
} }
if string(b) != string(testImportReqs[0].Body) { if string(b) != string(testImportReqs[0].Body)+"\r\n" {
t.Errorf("Invalid message body: expected %v but got %v", string(testImportReqs[0].Body), string(b)) t.Errorf("Invalid message body: expected %v but got %v", string(testImportReqs[0].Body), string(b))
} }

View File

@ -19,6 +19,7 @@ package pmapi
import ( import (
"bytes" "bytes"
"encoding/base64"
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil" "io/ioutil"
@ -289,3 +290,57 @@ func signAttachment(encrypter *crypto.KeyRing, data io.Reader) (signature io.Rea
} }
return bytes.NewReader(sig.GetBinary()), nil return bytes.NewReader(sig.GetBinary()), nil
} }
func encryptAndEncodeSessionKeys(
pubkey *crypto.KeyRing,
bodyKey *crypto.SessionKey,
attkeys map[string]*crypto.SessionKey,
) (bodyPacket string, attachmentPackets map[string]string, err error) {
// Encrypt message body keys.
packetBytes, err := pubkey.EncryptSessionKey(bodyKey)
if err != nil {
return
}
bodyPacket = base64.StdEncoding.EncodeToString(packetBytes)
// Encrypt attachment keys.
attachmentPackets = make(map[string]string)
for id, attkey := range attkeys {
var packets []byte
if packets, err = pubkey.EncryptSessionKey(attkey); err != nil {
return
}
attachmentPackets[id] = base64.StdEncoding.EncodeToString(packets)
}
return
}
func encryptSymmDecryptKey(
kr *crypto.KeyRing,
textToEncrypt string,
) (decryptedKey *crypto.SessionKey, symEncryptedData []byte, err error) {
// We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones).
firstKey, err := kr.FirstKey()
if err != nil {
return
}
pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr)
if err != nil {
return
}
pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0)
if err != nil {
return
}
decryptedKey, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket())
if err != nil {
return
}
symEncryptedData = pgpSplitMessage.GetBinaryDataPacket()
return
}

367
pkg/pmapi/message_send.go Normal file
View File

@ -0,0 +1,367 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package pmapi
import (
"encoding/base64"
"errors"
"github.com/ProtonMail/gopenpgp/v2/crypto"
)
// Draft actions
const (
DraftActionReply = 0
DraftActionReplyAll = 1
DraftActionForward = 2
)
// PackageFlag for send message package types
type PackageFlag int
func (p *PackageFlag) Has(flag PackageFlag) bool { return iHasFlag(int(*p), int(flag)) }
func (p *PackageFlag) HasAtLeastOne(flag PackageFlag) bool {
return iHasAtLeastOneFlag(int(*p), int(flag))
}
func (p *PackageFlag) Is(flag PackageFlag) bool { return iIsFlag(int(*p), int(flag)) }
func (p *PackageFlag) HasNo(flag PackageFlag) bool { return iHasNoneOfFlag(int(*p), int(flag)) }
// Send message package types.
const (
InternalPackage = PackageFlag(1)
EncryptedOutsidePackage = PackageFlag(2)
ClearPackage = PackageFlag(4)
PGPInlinePackage = PackageFlag(8)
PGPMIMEPackage = PackageFlag(16)
ClearMIMEPackage = PackageFlag(32)
)
// SignatureFlag for send signature types.
type SignatureFlag int
func (p *SignatureFlag) Is(flag SignatureFlag) bool { return iIsFlag(int(*p), int(flag)) }
func (p *SignatureFlag) Has(flag SignatureFlag) bool { return iHasFlag(int(*p), int(flag)) }
func (p *SignatureFlag) HasNo(flag SignatureFlag) bool { return iHasNoneOfFlag(int(*p), int(flag)) }
// Send signature types.
const (
SignatureNone = SignatureFlag(0)
SignatureDetached = SignatureFlag(1)
SignatureAttachedArmored = SignatureFlag(2)
)
// DraftReq defines paylod for creating drafts
type DraftReq struct {
Message *Message
ParentID string `json:",omitempty"`
Action int
AttachmentKeyPackets []string
}
func (c *client) CreateDraft(m *Message, parent string, action int) (created *Message, err error) {
createReq := &DraftReq{Message: m, ParentID: parent, Action: action, AttachmentKeyPackets: []string{}}
req, err := c.NewJSONRequest("POST", "/mail/v4/messages", createReq)
if err != nil {
return
}
var res MessageRes
if err = c.DoJSON(req, &res); err != nil {
return
}
created, err = res.Message, res.Err()
return
}
type AlgoKey struct {
Key string
Algorithm string
}
type MessageAddress struct {
Type PackageFlag
EncryptedBodyKeyPacket string `json:"BodyKeyPacket"` // base64-encoded key packet.
Signature SignatureFlag
EncryptedAttachmentKeyPackets map[string]string `json:"AttachmentKeyPackets"`
}
type MessagePackage struct {
Addresses map[string]*MessageAddress
Type PackageFlag
MIMEType string
EncryptedBody string `json:"Body"` // base64-encoded encrypted data packet.
DecryptedBodyKey AlgoKey `json:"BodyKey"` // base64-encoded session key (only if cleartext recipients).
DecryptedAttachmentKeys map[string]AlgoKey `json:"AttachmentKeys"` // Only include if cleartext & attachments.
}
func newMessagePackage(
send sendData,
attKeys map[string]AlgoKey,
) (pkg *MessagePackage) {
pkg = &MessagePackage{
EncryptedBody: base64.StdEncoding.EncodeToString(send.ciphertext),
Addresses: send.addressMap,
MIMEType: send.contentType,
Type: send.sharedScheme,
}
if send.sharedScheme.HasAtLeastOne(ClearPackage | ClearMIMEPackage) {
pkg.DecryptedBodyKey.Key = send.decryptedBodyKey.GetBase64Key()
pkg.DecryptedBodyKey.Algorithm = send.decryptedBodyKey.Algo
}
if len(attKeys) != 0 && send.sharedScheme.Has(ClearPackage) {
pkg.DecryptedAttachmentKeys = attKeys
}
return pkg
}
type sendData struct {
decryptedBodyKey *crypto.SessionKey //body session key
addressMap map[string]*MessageAddress
sharedScheme PackageFlag
ciphertext []byte
cleartext string
contentType string
}
type SendMessageReq struct {
ExpirationTime int64 `json:",omitempty"`
// AutoSaveContacts int `json:",omitempty"`
// Data for encrypted recipients.
Packages []*MessagePackage
mime, plain, rich sendData
attKeys map[string]*crypto.SessionKey
kr *crypto.KeyRing
}
func NewSendMessageReq(
kr *crypto.KeyRing,
mimeBody, plainBody, richBody string,
attKeys map[string]*crypto.SessionKey,
) *SendMessageReq {
req := &SendMessageReq{}
req.mime.addressMap = make(map[string]*MessageAddress)
req.plain.addressMap = make(map[string]*MessageAddress)
req.rich.addressMap = make(map[string]*MessageAddress)
req.mime.cleartext = mimeBody
req.plain.cleartext = plainBody
req.rich.cleartext = richBody
req.attKeys = attKeys
req.kr = kr
return req
}
var (
errUnknownContentType = errors.New("unknown content type")
errMultipartInNonMIME = errors.New("multipart mixed not allowed in this scheme")
errAttSignNotSupported = errors.New("attached signature not supported")
errEncryptMustSign = errors.New("encrypted package must be signed")
errEncryptedOutsideNotSupported = errors.New("encrypted outside is not supported")
errWrongSendScheme = errors.New("wrong send scheme")
errInternalMustEncrypt = errors.New("internal package must be encrypted")
errInlineMustBePlain = errors.New("PGP Inline package must be plain text")
errMissingPubkey = errors.New("cannot encrypt body key packet: missing pubkey")
errClearSignMustNotBeHTML = errors.New("clear signed packet must be multipart or plain")
errMIMEMustBeMultipart = errors.New("MIME packet must be multipart")
errClearMIMEMustSign = errors.New("clear MIME must be signed")
errClearSignMustNotBePGPInline = errors.New("clear sign must not be PGP inline")
)
func (req *SendMessageReq) AddRecipient(
email string, sendScheme PackageFlag,
pubkey *crypto.KeyRing, signature SignatureFlag,
contentType string, doEncrypt bool,
) (err error) {
if signature.Has(SignatureAttachedArmored) {
return errAttSignNotSupported
}
if doEncrypt && signature.HasNo(SignatureDetached) {
return errEncryptMustSign
}
switch sendScheme {
case PGPMIMEPackage, ClearMIMEPackage:
if contentType != ContentTypeMultipartMixed {
return errMIMEMustBeMultipart
}
return req.addMIMERecipient(email, sendScheme, pubkey, signature)
case InternalPackage, ClearPackage, PGPInlinePackage:
if contentType == ContentTypeMultipartMixed {
return errMultipartInNonMIME
}
return req.addNonMIMERecipient(email, sendScheme, pubkey, signature, contentType, doEncrypt)
case EncryptedOutsidePackage:
return errEncryptedOutsideNotSupported
default:
return errWrongSendScheme
}
}
func (req *SendMessageReq) addNonMIMERecipient(
email string, sendScheme PackageFlag,
pubkey *crypto.KeyRing, signature SignatureFlag,
contentType string, doEncrypt bool,
) (err error) {
if signature.Is(SignatureDetached) && !doEncrypt {
if sendScheme.Is(PGPInlinePackage) {
return errClearSignMustNotBePGPInline
}
if sendScheme.Is(ClearPackage) && contentType == ContentTypeHTML {
return errClearSignMustNotBeHTML
}
}
var send *sendData
switch contentType {
case ContentTypePlainText:
send = &req.plain
send.contentType = ContentTypePlainText
case ContentTypeHTML, "":
send = &req.rich
send.contentType = ContentTypeHTML
case ContentTypeMultipartMixed:
return errMultipartInNonMIME
default:
return errUnknownContentType
}
if send.decryptedBodyKey == nil {
if send.decryptedBodyKey, send.ciphertext, err = encryptSymmDecryptKey(req.kr, send.cleartext); err != nil {
return err
}
}
newAddress := &MessageAddress{Type: sendScheme, Signature: signature}
if sendScheme.Is(PGPInlinePackage) && contentType == ContentTypeHTML {
return errInlineMustBePlain
}
if sendScheme.Is(InternalPackage) && !doEncrypt {
return errInternalMustEncrypt
}
if doEncrypt && pubkey == nil {
return errMissingPubkey
}
if doEncrypt {
newAddress.EncryptedBodyKeyPacket, newAddress.EncryptedAttachmentKeyPackets, err = encryptAndEncodeSessionKeys(pubkey, send.decryptedBodyKey, req.attKeys)
if err != nil {
return err
}
}
send.addressMap[email] = newAddress
send.sharedScheme |= sendScheme
return nil
}
func (req *SendMessageReq) addMIMERecipient(
email string, sendScheme PackageFlag,
pubkey *crypto.KeyRing, signature SignatureFlag,
) (err error) {
if sendScheme.Is(ClearMIMEPackage) && signature.HasNo(SignatureDetached) {
return errClearMIMEMustSign
}
req.mime.contentType = ContentTypeMultipartMixed
if req.mime.decryptedBodyKey == nil {
if req.mime.decryptedBodyKey, req.mime.ciphertext, err = encryptSymmDecryptKey(req.kr, req.mime.cleartext); err != nil {
return err
}
}
if sendScheme.Is(PGPMIMEPackage) {
if pubkey == nil {
return errMissingPubkey
}
// Attachment keys are not needed because attachments are part
// of MIME body and therefore attachments are encrypted with
// body session key.
mimeBodyPacket, _, err := encryptAndEncodeSessionKeys(pubkey, req.mime.decryptedBodyKey, map[string]*crypto.SessionKey{})
if err != nil {
return err
}
req.mime.addressMap[email] = &MessageAddress{Type: sendScheme, EncryptedBodyKeyPacket: mimeBodyPacket, Signature: signature}
} else {
req.mime.addressMap[email] = &MessageAddress{Type: sendScheme, Signature: signature}
}
req.mime.sharedScheme |= sendScheme
return nil
}
func (req *SendMessageReq) PreparePackages() {
attkeysEncoded := make(map[string]AlgoKey)
for attID, attkey := range req.attKeys {
attkeysEncoded[attID] = AlgoKey{
Key: attkey.GetBase64Key(),
Algorithm: attkey.Algo,
}
}
for _, send := range []sendData{req.mime, req.plain, req.rich} {
if len(send.addressMap) == 0 {
continue
}
req.Packages = append(req.Packages, newMessagePackage(send, attkeysEncoded))
}
}
type SendMessageRes struct {
Res
Sent *Message
// Parent is only present if the sent message has a parent (reply/reply all/forward).
Parent *Message
}
func (c *client) SendMessage(id string, sendReq *SendMessageReq) (sent, parent *Message, err error) {
if id == "" {
err = errors.New("pmapi: cannot send message with an empty id")
return
}
if sendReq.Packages == nil {
sendReq.Packages = []*MessagePackage{}
}
req, err := c.NewJSONRequest("POST", "/mail/v4/messages/"+id, sendReq)
if err != nil {
return
}
var res SendMessageRes
if err = c.DoJSON(req, &res); err != nil {
return
}
sent, parent, err = res.Sent, res.Parent, res.Err()
return
}

View File

@ -0,0 +1,617 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package pmapi
import (
"encoding/base64"
"testing"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/stretchr/testify/require"
)
type recipient struct {
email string
sendScheme PackageFlag
pubkey *crypto.KeyRing
signature SignatureFlag
contentType string
doEncrypt bool
wantError error
}
type testData struct {
emails []string
recipients []recipient
wantPackages []*MessagePackage
allRecipients map[string]recipient
allAddresses map[string]*MessageAddress
attKeys map[string]*crypto.SessionKey
mimeBody, plainBody, richBody string
}
func (td *testData) addRecipients(t testing.TB) {
for _, email := range td.emails {
rcp, ok := td.allRecipients[email]
require.True(t, ok, "missing recipient %s", email)
rcp.email = email
td.recipients = append(td.recipients, rcp)
}
}
func (td *testData) addAddresses(t testing.TB) {
for i, wantPackage := range td.wantPackages {
for email := range wantPackage.Addresses {
address, ok := td.allAddresses[email]
require.True(t, ok, "missing address %s", email)
td.wantPackages[i].Addresses[email] = address
}
}
}
func (td *testData) prepareAndCheck(t *testing.T) {
r := require.New(t)
matchPresence := func(want string) require.ValueAssertionFunc {
if len(want) == 0 {
return require.Empty
}
return require.NotEmpty
}
have := NewSendMessageReq(testPrivateKeyRing, td.mimeBody, td.plainBody, td.richBody, td.attKeys)
for _, rec := range td.recipients {
err := have.AddRecipient(rec.email, rec.sendScheme, rec.pubkey, rec.signature, rec.contentType, rec.doEncrypt)
if rec.wantError == nil {
r.NoError(err, "email %s", rec.email)
} else {
r.EqualError(err, rec.wantError.Error(), "email %s", rec.email)
}
}
have.PreparePackages()
r.Equal(len(td.wantPackages), len(have.Packages))
for i, wantPackage := range td.wantPackages {
havePackage := have.Packages[i]
r.Equal(wantPackage.MIMEType, havePackage.MIMEType, "pkg %d", i)
r.Equal(wantPackage.Type, havePackage.Type, "pkg %d", i)
r.Equal(len(wantPackage.Addresses), len(havePackage.Addresses), "pkg %d", i)
for email, wantAddress := range wantPackage.Addresses {
haveAddress, ok := havePackage.Addresses[email]
r.True(ok, "pkg %d email %s", i, email)
r.Equal(wantAddress.Type, haveAddress.Type, "pkg %d email %s", i, email)
matchPresence(wantAddress.EncryptedBodyKeyPacket)(t, haveAddress.EncryptedBodyKeyPacket, "pkg %d email %s", i, email)
r.Equal(wantAddress.Signature, haveAddress.Signature, "pkg %d email %s", i, email)
if len(td.attKeys) == 0 {
r.Len(haveAddress.EncryptedAttachmentKeyPackets, 0)
} else {
r.Equal(
len(wantAddress.EncryptedAttachmentKeyPackets),
len(haveAddress.EncryptedAttachmentKeyPackets),
"pkg %d email %s", i, email,
)
for attID, wantAttKey := range wantAddress.EncryptedAttachmentKeyPackets {
haveAttKey, ok := haveAddress.EncryptedAttachmentKeyPackets[attID]
r.True(ok, "pkg %d email %s att %s", i, email, attID)
matchPresence(wantAttKey)(t, haveAttKey, "pkg %d email %s att %s", i, email, attID)
}
}
}
matchPresence(wantPackage.EncryptedBody)(t, havePackage.EncryptedBody, "pkg %d", i)
wantBodyKey := wantPackage.DecryptedBodyKey
haveBodyKey := havePackage.DecryptedBodyKey
matchPresence(wantBodyKey.Algorithm)(t, haveBodyKey.Algorithm, "pkg %d", i)
matchPresence(wantBodyKey.Key)(t, haveBodyKey.Key, "pkg %d", i)
if len(td.attKeys) == 0 {
r.Len(havePackage.DecryptedAttachmentKeys, 0)
} else {
r.Equal(
len(wantPackage.DecryptedAttachmentKeys),
len(havePackage.DecryptedAttachmentKeys),
"pkg %d", i,
)
for attID, wantAttKey := range wantPackage.DecryptedAttachmentKeys {
haveAttKey, ok := havePackage.DecryptedAttachmentKeys[attID]
r.True(ok, "pkg %d att %s", i, attID)
matchPresence(wantAttKey.Key)(t, haveAttKey.Key, "pkg %d att %s", i, attID)
matchPresence(wantAttKey.Algorithm)(t, haveAttKey.Algorithm, "pkg %d att %s", i, attID)
}
}
}
}
func TestSendReq(t *testing.T) {
attKeyB64 := "EvjO/2RIJNn6HdoU6ACqFdZglzJhpjQ/PpjsvL3mB5Q="
token, err := base64.StdEncoding.DecodeString(attKeyB64)
require.NoError(t, err)
attKey := crypto.NewSessionKeyFromToken(token, "aes256")
attKeyPackets := map[string]string{"attID": "not-empty"}
attAlgoKeys := map[string]AlgoKey{"attID": {"not-empty", "not-empty"}}
allRecipients := map[string]recipient{
// Internal OK
"none@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, "", true, nil},
"html@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil},
"plain@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil},
// Internal bad
"wrongtype@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, "application/rfc822", true, errUnknownContentType},
"multipart@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, errMultipartInNonMIME},
"noencrypt@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, false, errInternalMustEncrypt},
"no-pubkey@pm.me": {"", InternalPackage, nil, SignatureDetached, ContentTypeHTML, true, errMissingPubkey},
"nosigning@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureNone, ContentTypeHTML, true, errEncryptMustSign},
// testing combination
"internal1@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil},
// Clear OK
"html@email.com": {"", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil},
"none@email.com": {"", ClearPackage, nil, SignatureNone, "", false, nil},
"plain@email.com": {"", ClearPackage, nil, SignatureNone, ContentTypePlainText, false, nil},
"plain-sign@email.com": {"", ClearPackage, nil, SignatureDetached, ContentTypePlainText, false, nil},
"mime-sign@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, false, nil},
// Clear bad
"mime@email.com": {"", ClearMIMEPackage, nil, SignatureNone, ContentTypeMultipartMixed, false, errClearMIMEMustSign},
"clear-plain-sign@email.com": {"", PGPInlinePackage, nil, SignatureDetached, ContentTypePlainText, false, errClearSignMustNotBePGPInline},
"html-sign@email.com": {"", ClearPackage, nil, SignatureDetached, ContentTypeHTML, false, errClearSignMustNotBeHTML},
"mime-plain@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypePlainText, false, errMIMEMustBeMultipart},
"mime-html@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypeHTML, false, errMIMEMustBeMultipart},
// External Encryption OK
"mime@gpg.com": {"", PGPMIMEPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, nil},
"plain@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil},
// External Encryption bad
"eo@gpg.com": {"", EncryptedOutsidePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, errEncryptedOutsideNotSupported},
"inline-html@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, errInlineMustBePlain},
"inline-mixed@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, errMultipartInNonMIME},
"mime-plain@gpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypePlainText, true, errMIMEMustBeMultipart},
"mime-html@sgpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypeHTML, true, errMIMEMustBeMultipart},
"no-pubkey@gpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, true, errMissingPubkey},
"not-signed@gpg.com": {"", PGPMIMEPackage, testPublicKeyRing, SignatureNone, ContentTypeMultipartMixed, true, errEncryptMustSign},
}
allAddresses := map[string]*MessageAddress{
"none@pm.me": {
Type: InternalPackage,
Signature: SignatureDetached,
EncryptedBodyKeyPacket: "not-empty",
EncryptedAttachmentKeyPackets: attKeyPackets,
},
"plain@pm.me": {
Type: InternalPackage,
Signature: SignatureDetached,
EncryptedBodyKeyPacket: "not-empty",
EncryptedAttachmentKeyPackets: attKeyPackets,
},
"html@pm.me": {
Type: InternalPackage,
Signature: SignatureDetached,
EncryptedBodyKeyPacket: "not-empty",
EncryptedAttachmentKeyPackets: attKeyPackets,
},
"internal1@pm.me": {
Type: InternalPackage,
Signature: SignatureDetached,
EncryptedBodyKeyPacket: "not-empty",
EncryptedAttachmentKeyPackets: attKeyPackets,
},
"html@email.com": {
Type: ClearPackage,
Signature: SignatureNone,
},
"none@email.com": {
Type: ClearPackage,
Signature: SignatureNone,
},
"plain@email.com": {
Type: ClearPackage,
Signature: SignatureNone,
},
"plain-sign@email.com": {
Type: ClearPackage,
Signature: SignatureDetached,
},
"mime-sign@email.com": {
Type: ClearMIMEPackage,
Signature: SignatureDetached,
},
"mime@gpg.com": {
Type: PGPMIMEPackage,
Signature: SignatureDetached,
EncryptedBodyKeyPacket: "non-empty",
},
"plain@gpg.com": {
Type: PGPInlinePackage,
Signature: SignatureDetached,
EncryptedBodyKeyPacket: "non-empty",
EncryptedAttachmentKeyPackets: attKeyPackets,
},
}
// NOTE naming
// Single: there should be one package
// Multiple: there should be more than one package
// Internal: there should be internal package
// Clear: there should be non-encrypted package
// Encrypted: there should be encrypted package
// NotAllowed: combination of inputs which are not allowed
newTests := map[string]testData{
"Nothing": { // expect no crash
emails: []string{},
wantPackages: []*MessagePackage{},
},
"Fails": {
emails: []string{
"wrongtype@pm.me",
"multipart@pm.me",
"noencrypt@pm.me",
"no-pubkey@pm.me",
"nosigning@pm.me",
"html-sign@email.com",
"mime-plain@email.com",
"mime-html@email.com",
"mime@email.com",
"clear-plain-sign@email.com",
"eo@gpg.com",
"inline-html@gpg.com",
"inline-mixed@gpg.com",
"mime-plain@gpg.com",
"mime-html@sgpg.com",
"no-pubkey@gpg.com",
"not-signed@gpg.com",
},
},
// one scheme in one package
"SingleInternalHTML": {
emails: []string{"none@pm.me", "html@pm.me"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"none@pm.me": nil,
"html@pm.me": nil,
},
Type: InternalPackage,
MIMEType: ContentTypeHTML,
EncryptedBody: "non-empty",
},
},
},
"SingleInternalPlain": {
emails: []string{"plain@pm.me"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"plain@pm.me": nil,
},
Type: InternalPackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
},
},
},
"SingleClearHTML": {
emails: []string{"none@email.com", "html@email.com"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"html@email.com": nil,
"none@email.com": nil,
},
Type: ClearPackage,
MIMEType: ContentTypeHTML,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
DecryptedAttachmentKeys: attAlgoKeys,
},
},
},
"SingleClearPlain": {
emails: []string{"plain@email.com", "plain-sign@email.com"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"plain@email.com": nil,
"plain-sign@email.com": nil,
},
Type: ClearPackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
DecryptedAttachmentKeys: attAlgoKeys,
},
},
},
"SingleClearMIME": {
emails: []string{"mime-sign@email.com"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"mime-sign@email.com": nil,
},
Type: ClearMIMEPackage,
MIMEType: ContentTypeMultipartMixed,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
},
},
},
"SingleEncyptedPlain": {
emails: []string{"plain@gpg.com"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"plain@gpg.com": nil,
},
Type: PGPInlinePackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
},
},
},
"SingleEncyptedMIME": {
emails: []string{"mime@gpg.com"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"mime@gpg.com": nil,
},
Type: PGPMIMEPackage,
MIMEType: ContentTypeMultipartMixed,
EncryptedBody: "non-empty",
},
},
},
// two schemes combined to one package
"SingleClearInternalPlain": {
emails: []string{"plain@email.com", "plain-sign@email.com", "plain@pm.me"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"plain@pm.me": nil,
"plain@email.com": nil,
"plain-sign@email.com": nil,
},
Type: InternalPackage | ClearPackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
DecryptedAttachmentKeys: attAlgoKeys,
},
},
},
"SingleClearInternalHTML": {
emails: []string{"none@email.com", "html@email.com", "html@pm.me", "none@pm.me"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"none@pm.me": nil,
"html@pm.me": nil,
"html@email.com": nil,
"none@email.com": nil,
},
Type: InternalPackage | ClearPackage,
MIMEType: ContentTypeHTML,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
DecryptedAttachmentKeys: attAlgoKeys,
},
},
},
"SingleEncryptedInternalPlain": {
emails: []string{"plain@gpg.com", "plain@pm.me"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"plain@pm.me": nil,
"plain@gpg.com": nil,
},
Type: InternalPackage | PGPInlinePackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
},
},
},
"SingleEncryptedClearMIME": {
emails: []string{"mime@gpg.com", "mime-sign@email.com"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"mime@gpg.com": nil,
"mime-sign@email.com": nil,
},
Type: ClearMIMEPackage | PGPMIMEPackage,
MIMEType: ContentTypeMultipartMixed,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
},
},
},
// one scheme separated to multiple packages
"MultipleInternal": {
emails: []string{"none@pm.me", "html@pm.me", "plain@pm.me"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"plain@pm.me": nil,
},
Type: InternalPackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
},
{
Addresses: map[string]*MessageAddress{
"none@pm.me": nil,
"html@pm.me": nil,
},
Type: InternalPackage,
MIMEType: ContentTypeHTML,
EncryptedBody: "non-empty",
},
},
},
"MultipleClear": {
emails: []string{
"none@email.com", "html@email.com",
"plain@email.com", "plain-sign@email.com",
"mime-sign@email.com",
},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"mime-sign@email.com": nil,
},
Type: ClearMIMEPackage,
MIMEType: ContentTypeMultipartMixed,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
},
{
Addresses: map[string]*MessageAddress{
"plain@email.com": nil,
"plain-sign@email.com": nil,
},
Type: ClearPackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
DecryptedAttachmentKeys: attAlgoKeys,
},
{
Addresses: map[string]*MessageAddress{
"html@email.com": nil,
"none@email.com": nil,
},
Type: ClearPackage,
MIMEType: ContentTypeHTML,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
DecryptedAttachmentKeys: attAlgoKeys,
},
},
},
"MultipleEncrypted": {
emails: []string{"plain@gpg.com", "mime@gpg.com"},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"mime@gpg.com": nil,
},
Type: PGPMIMEPackage,
MIMEType: ContentTypeMultipartMixed,
EncryptedBody: "non-empty",
},
{
Addresses: map[string]*MessageAddress{
"plain@gpg.com": nil,
},
Type: PGPInlinePackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
},
},
},
"MultipleComboAll": {
emails: []string{
"none@pm.me",
"plain@pm.me",
"html@pm.me",
"none@email.com",
"html@email.com",
"plain@email.com",
"plain-sign@email.com",
"mime-sign@email.com",
"mime@gpg.com",
"plain@gpg.com",
},
wantPackages: []*MessagePackage{
{
Addresses: map[string]*MessageAddress{
"mime@gpg.com": nil,
"mime-sign@email.com": nil,
},
Type: ClearMIMEPackage | PGPMIMEPackage,
MIMEType: ContentTypeMultipartMixed,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
},
{
Addresses: map[string]*MessageAddress{
"plain@gpg.com": nil,
"plain@email.com": nil,
"plain-sign@email.com": nil,
"plain@pm.me": nil,
},
Type: InternalPackage | ClearPackage | PGPInlinePackage,
MIMEType: ContentTypePlainText,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
DecryptedAttachmentKeys: attAlgoKeys,
},
{
Addresses: map[string]*MessageAddress{
"none@pm.me": nil,
"html@pm.me": nil,
"none@email.com": nil,
"html@email.com": nil,
},
Type: InternalPackage | ClearPackage,
MIMEType: ContentTypeHTML,
EncryptedBody: "non-empty",
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
DecryptedAttachmentKeys: attAlgoKeys,
},
},
},
}
for name, test := range newTests {
test.mimeBody = "Mime body"
test.plainBody = "Plain body"
test.richBody = "HTML body"
test.allRecipients = allRecipients
test.allAddresses = allAddresses
test.addRecipients(t)
test.addAddresses(t)
t.Run("NoAtt"+name, test.prepareAndCheck)
test.attKeys = map[string]*crypto.SessionKey{"attID": attKey}
t.Run("Att"+name, test.prepareAndCheck)
}
}

View File

@ -253,6 +253,10 @@ func (m *Message) HasLabelID(labelID string) bool {
return false return false
} }
func (m *Message) IsEncrypted() bool {
return strings.HasPrefix(m.Header.Get("Content-Type"), "multipart/encrypted") || m.IsBodyEncrypted()
}
func (m *Message) IsBodyEncrypted() bool { func (m *Message) IsBodyEncrypted() bool {
trimmedBody := strings.TrimSpace(m.Body) trimmedBody := strings.TrimSpace(m.Body)
return strings.HasPrefix(trimmedBody, MessageHeader) && return strings.HasPrefix(trimmedBody, MessageHeader) &&
@ -569,114 +573,6 @@ func (c *client) GetMessage(id string) (msg *Message, err error) {
return res.Message, res.Err() return res.Message, res.Err()
} }
type SendMessageReq struct {
ExpirationTime int64 `json:",omitempty"`
// AutoSaveContacts int `json:",omitempty"`
// Data for encrypted recipients.
Packages []*MessagePackage
}
// Message package types.
const (
InternalPackage = 1
EncryptedOutsidePackage = 2
ClearPackage = 4
PGPInlinePackage = 8
PGPMIMEPackage = 16
ClearMIMEPackage = 32
)
// Signature types.
const (
NoSignature = 0
YesSignature = 1
)
type MessagePackage struct {
Addresses map[string]*MessageAddress
Type int
MIMEType string
Body string // base64-encoded encrypted data packet.
BodyKey AlgoKey // base64-encoded session key (only if cleartext recipients).
AttachmentKeys map[string]AlgoKey // Only include if cleartext & attachments.
}
type MessageAddress struct {
Type int
BodyKeyPacket string // base64-encoded key packet.
Signature int // 0 = None, 1 = Detached, 2 = Attached/Armored
AttachmentKeyPackets map[string]string
}
type AlgoKey struct {
Key string
Algorithm string
}
type SendMessageRes struct {
Res
Sent *Message
// Parent is only present if the sent message has a parent (reply/reply all/forward).
Parent *Message
}
func (c *client) SendMessage(id string, sendReq *SendMessageReq) (sent, parent *Message, err error) {
if id == "" {
err = errors.New("pmapi: cannot send message with an empty id")
return
}
if sendReq.Packages == nil {
sendReq.Packages = []*MessagePackage{}
}
req, err := c.NewJSONRequest("POST", "/mail/v4/messages/"+id, sendReq)
if err != nil {
return
}
var res SendMessageRes
if err = c.DoJSON(req, &res); err != nil {
return
}
sent, parent, err = res.Sent, res.Parent, res.Err()
return
}
const (
DraftActionReply = 0
DraftActionReplyAll = 1
DraftActionForward = 2
)
type DraftReq struct {
Message *Message
ParentID string `json:",omitempty"`
Action int
AttachmentKeyPackets []string
}
func (c *client) CreateDraft(m *Message, parent string, action int) (created *Message, err error) {
createReq := &DraftReq{Message: m, ParentID: parent, Action: action, AttachmentKeyPackets: []string{}}
req, err := c.NewJSONRequest("POST", "/mail/v4/messages", createReq)
if err != nil {
return
}
var res MessageRes
if err = c.DoJSON(req, &res); err != nil {
return
}
created, err = res.Message, res.Err()
return
}
type MessagesActionReq struct { type MessagesActionReq struct {
IDs []string IDs []string
} }

View File

@ -85,7 +85,7 @@ type MailSettings struct {
RightToLeft int RightToLeft int
AttachPublicKey int AttachPublicKey int
Sign int Sign int
PGPScheme int PGPScheme PackageFlag
PromptPin int PromptPin int
Autocrypt int Autocrypt int
NumMessagePerPage int NumMessagePerPage int

View File

@ -18,7 +18,7 @@
package pmapi package pmapi
import ( import (
"github.com/getsentry/raven-go" "github.com/getsentry/sentry-go"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -119,7 +119,11 @@ func (c *client) UpdateUser() (user *User, err error) {
} }
c.user = user c.user = user
raven.SetUserContext(&raven.User{ID: user.ID}) sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetUser(sentry.User{
ID: user.ID,
})
})
var tmpList AddressList var tmpList AddressList
if tmpList, err = c.GetAddresses(); err == nil { if tmpList, err = c.GetAddresses(); err == nil {

23
pkg/pmapi/utils.go Normal file
View File

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

View File

@ -18,138 +18,23 @@
package sentry package sentry
import ( import (
"fmt" "errors"
"regexp"
"runtime" "runtime"
"runtime/pprof" "time"
"strconv"
"strings"
"github.com/getsentry/raven-go" "github.com/getsentry/sentry-go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
const fileParseError = "[file parse error]" var (
skippedFunctions = []string{} //nolint[gochecknoglobals]
)
var isGoroutine = regexp.MustCompile("^goroutine [[:digit:]]+.*") //nolint[gochecknoglobals] // ReportSentryCrash reports a sentry crash.
func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error) error {
// Threads implements standard sentry thread report. SkipDuringUnwind()
type Threads struct {
Values []Thread `json:"values"`
}
// Class specifier.
func (s *Threads) Class() string { return "threads" }
// Thread wraps a single stacktrace.
type Thread struct {
ID int `json:"id"`
Name string `json:"name"`
Crashed bool `json:"crashed"`
Stacktrace *raven.Stacktrace `json:"stacktrace"`
}
// TraceAllRoutines traces all goroutines and saves them to the current object.
func (s *Threads) TraceAllRoutines() {
s.Values = []Thread{}
goroutines := &strings.Builder{}
_ = pprof.Lookup("goroutine").WriteTo(goroutines, 2)
thread := Thread{ID: -1}
var frame *raven.StacktraceFrame
for _, v := range strings.Split(goroutines.String(), "\n") {
// Ignore empty lines.
if v == "" {
continue
}
// New routine.
if isGoroutine.MatchString(v) {
if thread.ID >= 0 {
s.Values = append(s.Values, thread)
}
thread = Thread{ID: thread.ID + 1, Name: v, Crashed: thread.ID == -1, Stacktrace: &raven.Stacktrace{Frames: []*raven.StacktraceFrame{}}}
continue
}
// New function.
if frame == nil {
frame = &raven.StacktraceFrame{Function: v}
continue
}
// Set filename and add frame.
if frame.Filename == "" {
fld := strings.Fields(v)
if len(fld) != 2 {
frame.Filename = fileParseError
frame.AbsolutePath = v
} else {
frame.Filename = fld[0]
sp := strings.Split(fld[0], ":")
if len(sp) > 1 {
i, err := strconv.Atoi(sp[len(sp)-1])
if err == nil {
frame.Filename = strings.Join(sp[:len(sp)-1], ":")
frame.Lineno = i
}
}
}
if frame.AbsolutePath == "" && frame.Filename != fileParseError {
frame.AbsolutePath = frame.Filename
if sp := strings.Split(frame.Filename, "/"); len(sp) > 1 {
frame.Filename = sp[len(sp)-1]
}
}
thread.Stacktrace.Frames = append([]*raven.StacktraceFrame{frame}, thread.Stacktrace.Frames...)
frame = nil
continue
}
}
// Add last thread.
s.Values = append(s.Values, thread)
}
func findPanicSender(s *Threads, err error) string {
out := "error nil"
if err != nil {
out = err.Error()
}
for _, thread := range s.Values {
if !thread.Crashed {
continue
}
for i, fr := range thread.Stacktrace.Frames {
if strings.HasSuffix(fr.Filename, "panic.go") && strings.HasPrefix(fr.Function, "panic") {
// Next frame if any.
j := 0
if i > j {
j = i - 1
}
// Directory and filename.
fname := thread.Stacktrace.Frames[j].AbsolutePath
if sp := strings.Split(fname, "/"); len(sp) > 2 {
fname = strings.Join(sp[len(sp)-2:], "/")
}
// Line number.
if ln := thread.Stacktrace.Frames[j].Lineno; ln > 0 {
fname = fmt.Sprintf("%s:%d", fname, ln)
}
out = fmt.Sprintf("%s: %s", fname, out)
break // Just first panic.
}
}
}
return out
}
// ReportSentryCrash reports a sentry crash with stacktrace from all goroutines.
func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error) (err error) {
if reportErr == nil { if reportErr == nil {
return return nil
} }
tags := map[string]string{ tags := map[string]string{
@ -160,18 +45,71 @@ func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error)
"UserID": "", "UserID": "",
} }
threads := &Threads{} var reportID string
threads.TraceAllRoutines() sentry.WithScope(func(scope *sentry.Scope) {
errorWithFile := findPanicSender(threads, reportErr) SkipDuringUnwind()
packet := raven.NewPacket(errorWithFile, threads) scope.SetTags(tags)
eventID := sentry.CaptureException(reportErr)
if eventID != nil {
reportID = string(*eventID)
}
})
eventID, ch := raven.Capture(packet, tags) if !sentry.Flush(time.Second * 10) {
log.WithField("error", reportErr).Error("Failed to report sentry error")
if err = <-ch; err == nil { return errors.New("failed to report sentry error")
log.WithField("errorID", eventID).Warn("Reported sentry error")
} else {
log.WithField("error", reportErr).WithError(err).Error("Failed to report sentry error")
} }
return err log.WithField("error", reportErr).WithField("id", reportID).Warn("Sentry error reported")
return nil
}
// SkipDuringUnwind removes caller from the traceback.
func SkipDuringUnwind() {
pcs := make([]uintptr, 2)
n := runtime.Callers(2, pcs)
if n == 0 {
return
}
frames := runtime.CallersFrames(pcs)
frame, _ := frames.Next()
if isFunctionFilteredOut(frame.Function) {
return
}
skippedFunctions = append(skippedFunctions, frame.Function)
}
// EnhanceSentryEvent swaps type with value and removes panic handlers from the stacktrace.
func EnhanceSentryEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
for idx, exception := range event.Exception {
exception.Type, exception.Value = exception.Value, exception.Type
if exception.Stacktrace != nil {
exception.Stacktrace.Frames = filterOutPanicHandlers(exception.Stacktrace.Frames)
}
event.Exception[idx] = exception
}
return event
}
func filterOutPanicHandlers(frames []sentry.Frame) []sentry.Frame {
newFrames := []sentry.Frame{}
for _, frame := range frames {
// Sentry splits runtime.Frame.Function into Module and Function.
function := frame.Module + "." + frame.Function
if !isFunctionFilteredOut(function) {
newFrames = append(newFrames, frame)
}
}
return newFrames
}
func isFunctionFilteredOut(function string) bool {
for _, skipFunction := range skippedFunctions {
if function == skipFunction {
return true
}
}
return false
} }

View File

@ -18,52 +18,49 @@
package sentry package sentry
import ( import (
"errors"
"testing" "testing"
"github.com/getsentry/raven-go" r "github.com/stretchr/testify/require"
"github.com/getsentry/sentry-go"
) )
func TestSentryCrashReport(t *testing.T) { func TestSkipDuringUnwind(t *testing.T) {
if err := ReportSentryCrash( // More calls in one function adds it only once.
"clientID", SkipDuringUnwind()
"appVersion", SkipDuringUnwind()
"useragent", func() {
errors.New("Testing crash report - api proxy; goroutines with threads, find origin"), SkipDuringUnwind()
); err != nil { SkipDuringUnwind()
t.Fatal("Expected no error while report, but have", err) }()
wantSkippedFunctions := []string{
"github.com/ProtonMail/proton-bridge/pkg/sentry.TestSkipDuringUnwind",
"github.com/ProtonMail/proton-bridge/pkg/sentry.TestSkipDuringUnwind.func1",
} }
r.Equal(t, wantSkippedFunctions, skippedFunctions)
} }
func (s *Threads) TraceAllRoutinesTest() { func TestFilterOutPanicHandlers(t *testing.T) {
s.Values = []Thread{ skippedFunctions = []string{
{ "github.com/ProtonMail/proton-bridge/pkg/config.(*PanicHandler).HandlePanic",
ID: 0, "github.com/ProtonMail/proton-bridge/pkg/config.HandlePanic",
Name: "goroutine 20 [running]", "github.com/ProtonMail/proton-bridge/pkg/sentry.ReportSentryCrash",
Crashed: true, "github.com/ProtonMail/proton-bridge/pkg/sentry.ReportSentryCrash.func1",
Stacktrace: &raven.Stacktrace{
Frames: []*raven.StacktraceFrame{
{
Filename: "/home/dev/build/go-1.10.2/go/src/runtime/pprof/pprof.go",
Function: "runtime/pprof.writeGoroutineStacks(0x9b7de0, 0xc4203e2900, 0xd0, 0xd0)",
Lineno: 650,
},
},
},
},
{
ID: 1,
Name: "goroutine 20 [chan receive]",
Crashed: false,
Stacktrace: &raven.Stacktrace{
Frames: []*raven.StacktraceFrame{
{
Filename: "/home/dev/build/go-1.10.2/go/src/testing/testing.go",
Function: "testing.(*T).Run(0xc4203e42d0, 0x90f445, 0x15, 0x97d358, 0x47a501)",
Lineno: 825,
},
},
},
},
} }
frames := []sentry.Frame{
{Module: "github.com/ProtonMail/proton-bridge/internal/cmd", Function: "main"},
{Module: "github.com/urfave/cli", Function: "(*App).Run"},
{Module: "github.com/ProtonMail/proton-bridge/internal/cmd", Function: "RegisterHandlePanic"},
{Module: "github.com/ProtonMail/pkg", Function: "HandlePanic"},
{Module: "main", Function: "run"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "(*PanicHandler).HandlePanic"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "HandlePanic"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/sentry", Function: "ReportSentryCrash"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/sentry", Function: "ReportSentryCrash.func1"},
}
gotFrames := filterOutPanicHandlers(frames)
r.Equal(t, frames[:5], gotFrames)
} }

View File

@ -1,3 +1,3 @@
Ensured that conversations are properly threaded AppleMail crashes (timestamp related)
Fixed Linux font issues (Fedora) Encoding errors
Better handling of Mime encrypted messages Installation issues on linux

View File

@ -0,0 +1 @@
• Installation issues on linux

View File

@ -1,4 +1,3 @@
Ensured better message flow by refactoring both address and date parsing Support read confirmations
Improved secure connectivity checks Adding GPLv3 licence button to the GUI
Better deb packaging Improved testing
• More robust error handling

View File

@ -1,3 +1,3 @@
Further improvements to address and date parsing Allow an import of already encrypted messages (as cypher text)
Better handling and displaying of skipped messages Cosmetic GUI changes
Improved error reporting Better error handling

View File

@ -28,7 +28,7 @@ import (
func APIChecksFeatureContext(s *godog.Suite) { func APIChecksFeatureContext(s *godog.Suite) {
s.Step(`^API endpoint "([^"]*)" is called with:$`, apiIsCalledWith) s.Step(`^API endpoint "([^"]*)" is called with:$`, apiIsCalledWith)
s.Step(`^message is sent with API call:$`, messageIsSentWithAPICall) s.Step(`^message is sent with API call$`, messageIsSentWithAPICall)
s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages) s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages)
s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages) s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages)
} }

View File

@ -68,10 +68,11 @@ type TestContext struct {
smtpLastResponses map[string]*mocks.SMTPResponse smtpLastResponses map[string]*mocks.SMTPResponse
// Transfer related variables. // Transfer related variables.
transferLocalRootForImport string transferLocalRootForImport string
transferLocalRootForExport string transferLocalRootForExport string
transferRemoteIMAPServer *mocks.IMAPServer transferRemoteIMAPServer *mocks.IMAPServer
transferProgress *transfer.Progress transferProgress *transfer.Progress
transferSkipEncryptedMessages bool
// Store releated variables. // Store releated variables.
bddMessageIDsToAPIIDs map[string]string bddMessageIDsToAPIIDs map[string]string

View File

@ -37,6 +37,16 @@ func (ctx *TestContext) GetTransferProgress() *transfer.Progress {
return ctx.transferProgress return ctx.transferProgress
} }
// SetTransferSkipEncryptedMessages sets whether encrypted messages will be skipped.
func (ctx *TestContext) SetTransferSkipEncryptedMessages(value bool) {
ctx.transferSkipEncryptedMessages = value
}
// GetTransferSkipEncryptedMessages gets whether encrypted messages will be skipped.
func (ctx *TestContext) GetTransferSkipEncryptedMessages() bool {
return ctx.transferSkipEncryptedMessages
}
// GetTransferLocalRootForImport creates temporary root for importing // GetTransferLocalRootForImport creates temporary root for importing
// if it not exists yet, and returns its path. // if it not exists yet, and returns its path.
func (ctx *TestContext) GetTransferLocalRootForImport() string { func (ctx *TestContext) GetTransferLocalRootForImport() string {

View File

@ -20,8 +20,10 @@ package fakeapi
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/mail"
"strings" "strings"
messageUtils "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
@ -139,6 +141,7 @@ func (ctl *Controller) AddUserMessage(username string, message *pmapi.Message) (
} }
message.ID = ctl.messageIDGenerator.next("") message.ID = ctl.messageIDGenerator.next("")
message.LabelIDs = append(message.LabelIDs, pmapi.AllMailLabel) message.LabelIDs = append(message.LabelIDs, pmapi.AllMailLabel)
message.Header = mail.Header(messageUtils.GetHeader(message))
ctl.messagesByUsername[username] = append(ctl.messagesByUsername[username], message) ctl.messagesByUsername[username] = append(ctl.messagesByUsername[username], message)
ctl.resetUsers() ctl.resetUsers()
return message.ID, nil return message.ID, nil

View File

@ -60,3 +60,17 @@ Feature: IMAP fetch messages
When IMAP client fetches by UID "1:*" When IMAP client fetches by UID "1:*"
Then IMAP response is "OK" Then IMAP response is "OK"
And IMAP response has 2 message And IMAP response has 2 message
Scenario: Fetch of very old message sent from the moon succeeds with modified date
Given there are messages in mailbox "Folders/mbox" for "user"
| from | to | subject | time |
| john.doe@mail.com | user@pm.me | foo | 1969-07-20T00:00:00 |
And there is IMAP client logged in as "user"
And there is IMAP client selected in "Folders/mbox"
When IMAP client sends command "FETCH 1:* rfc822"
Then IMAP response is "OK"
And IMAP response contains "Date: Fri, 13 Aug 1982"
And IMAP response contains "X-Original-Date: Sun, 20 Jul 1969"
# We had bug to incorectly set empty date, so let's make sure
# there is no reference anywhere in the response.
And IMAP response does not contain "Date: Thu, 01 Jan 1970"

View File

@ -1,29 +1,29 @@
Feature: SMTP auth Feature: SMTP auth
Scenario: Ask EHLO Scenario: Ask EHLO
Given there is connected user "user" Given there is connected user "user"
When SMTP client sends EHLO When SMTP client sends "EHLO example.com"
Then SMTP response is "OK" Then SMTP response is "OK"
Scenario: Authenticates successfully and EHLO successfully Scenario: Authenticates successfully and EHLO successfully
Given there is connected user "user" Given there is connected user "user"
When SMTP client authenticates "user" When SMTP client authenticates "user"
Then SMTP response is "OK" Then SMTP response is "OK"
When SMTP client sends EHLO When SMTP client sends "EHLO example.com"
Then SMTP response is "OK" Then SMTP response is "OK"
Scenario: Authenticates with bad password Scenario: Authenticates with bad password
Given there is connected user "user" Given there is connected user "user"
When SMTP client authenticates "user" with bad password When SMTP client authenticates "user" with bad password
Then SMTP response is "SMTP error: 454 backend/credentials: incorrect password" Then SMTP response is "SMTP error: 454 4.7.0 backend/credentials: incorrect password"
Scenario: Authenticates with disconnected user Scenario: Authenticates with disconnected user
Given there is disconnected user "user" Given there is disconnected user "user"
When SMTP client authenticates "user" When SMTP client authenticates "user"
Then SMTP response is "SMTP error: 454 account is logged out, use the app to login again" Then SMTP response is "SMTP error: 454 4.7.0 account is logged out, use the app to login again"
Scenario: Authenticates with no user Scenario: Authenticates with no user
When SMTP client authenticates with username "user@pm.me" and password "bridgepassword" When SMTP client authenticates with username "user@pm.me" and password "bridgepassword"
Then SMTP response is "SMTP error: 454 user user@pm.me not found" Then SMTP response is "SMTP error: 454 4.7.0 user user@pm.me not found"
Scenario: Authenticates with capital letter Scenario: Authenticates with capital letter
Given there is connected user "userAddressWithCapitalLetter" Given there is connected user "userAddressWithCapitalLetter"
@ -43,7 +43,7 @@ Feature: SMTP auth
Scenario: Authenticates with more addresses - disabled address Scenario: Authenticates with more addresses - disabled address
Given there is connected user "userMoreAddresses" Given there is connected user "userMoreAddresses"
When SMTP client authenticates "userMoreAddresses" with address "disabled" When SMTP client authenticates "userMoreAddresses" with address "disabled"
Then SMTP response is "SMTP error: 454 user .* not found" Then SMTP response is "SMTP error: 454 4.7.0 user .* not found"
@ignore-live @ignore-live
Scenario: Authenticates with secondary address of account with disabled primary address Scenario: Authenticates with secondary address of account with disabled primary address

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