Compare commits

..

113 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
70645c1732 Import-Export Elbe 1.2.1
• Further improvements to address and date parsing
• Better handling and displaying of skipped messages
• Improved error reporting
2020-11-11 14:03:00 +01:00
1055e60d27 Fixing time order in changelog. 2020-11-11 12:02:56 +01:00
e04196f8a0 feat: switch to public go-rfc5322 parser 2020-11-10 09:27:07 +00:00
11a0dec047 Using atomic bool 2020-11-10 07:50:29 +00:00
b9740e1b7d Close connection before deleting labels to prevent panics accessing deleted bucket 2020-11-10 07:50:29 +00:00
f0695eb870 add test gui 2020-11-09 11:58:32 +00:00
a40018cdf9 Percentage available on progress count struct 2020-11-09 11:58:32 +00:00
5b7eabe21a Skipped messages do not change total counts but shows as separate number 2020-11-09 11:58:32 +00:00
d5d60aa11b feat: remove tls upgrade error notification 2020-11-09 10:59:42 +00:00
a62fa132e6 rename build tag 2020-11-06 16:02:30 +01:00
052395f917 test: add benchmarks for rfc5322 address/date parser 2020-11-04 15:00:18 +01:00
9a77650004 Bridge GoldenGate 1.5.0
- Ensured better message flow by refactoring both address and date parsing
- Improved secure connectivity checks
- Better deb packaging
- More robust error handling

- Ensured that conversations are properly threaded
- Fixed Linux font issues (Fedora)
- Better handling of Mime encrypted messages
2020-11-04 12:26:07 +01:00
f1d70361c9 Do not include conversation ID in references 2020-11-04 09:12:16 +00:00
3496599723 feat: custom address/date parser based on rfc5322 abnf 2020-11-03 16:21:06 +01:00
9e0635a6a4 fix: don't check tls fingerprints when checking connectivity 2020-11-02 13:38:39 +00:00
10509621ce Updated go-mbox dependency back to upstream 2020-11-02 10:32:21 +01:00
3727ecdfe5 Show in error counts also lost messages at the end report 2020-10-30 13:58:32 +00:00
ac71d22e86 Waiting for unilateral update during deleting the message 2020-10-30 13:42:04 +00:00
bc81356d53 test: update feature file to use new "seq" command 2020-10-29 13:10:54 +01:00
881cb64beb Release Danube: notes, version bump, change log 2020-10-29 12:57:59 +01:00
1286e57b63 Support Apple Mail MBOX export format 2020-10-29 09:07:37 +01:00
fe5f73d96e Fix crash when IMAP client connects while account is logging in 2020-10-29 07:21:45 +00:00
8f7a8b31a3 Apply 1 suggestion(s) to 1 file(s) 2020-10-28 16:42:57 +00:00
68db35d5d4 Not able to update I-E on mac GODT-794
Added missing signal, corrected the update name, log tweaks.
2020-10-28 16:42:57 +00:00
df17017ced Apply 1 suggestion(s) to 1 file(s) 2020-10-28 10:20:32 +00:00
5c48332b0e change rectangle to column in global settings GODT-677 2020-10-27 10:13:08 +01:00
8985738af5 Merge master into devel 2020-10-23 10:31:08 +02:00
2d8a676dd5 Merge branch 'release/forth' into release/danube 2020-10-22 18:00:33 +02:00
7e0a9f398c I/E Fix printing zero time in error report 2020-10-22 09:12:56 +00:00
9af5769510 Apply 1 suggestion(s) to 1 file(s) 2020-10-22 08:26:35 +00:00
bb46d9a009 README and BUILD info about Import-Export and tags 2020-10-22 10:22:00 +02:00
606b42a6e7 Fix flaky TestFailUnpauseAndStops 2020-10-22 10:04:22 +02:00
d547f5ea22 Changelog 2020-10-21 13:56:55 +02:00
563b4889e3 Update go-imap dependency to get fix for UTF-7 incompatibility 2020-10-21 09:15:42 +00:00
b449beb68c Do not spam sentry with bad ID by integration test 2020-10-21 08:38:54 +00:00
f9d58f4f9c Merge branch 'release/forth' into release/danube 2020-10-21 09:07:27 +02:00
1dfec9902e gofmt fix 2020-10-21 09:04:06 +02:00
79cafee2eb Support quoted printable and filter out some auto-generated Gmail labels 2020-10-21 09:04:06 +02:00
64fbcdc1ca Fix mbox scanning 2020-10-21 09:04:06 +02:00
e4a341af3a Better log message 2020-10-21 09:04:05 +02:00
e0292fe957 Use map instead of list as set 2020-10-21 09:04:05 +02:00
ef85c8df24 Detect Gmail labels from All Mail mbox export 2020-10-21 09:04:05 +02:00
719d369c2a Fix transfer stopping 2020-10-21 06:42:54 +00:00
51b6f95342 Show fatal errors after export is terminated 2020-10-21 06:14:39 +00:00
26fb1fc34d Sanizize mailbox name for exporting 2020-10-21 06:02:02 +00:00
ae1578a5e2 GODT-829 fix apple mail subfolders 2020-10-20 19:09:59 +02:00
cfd8e56277 Do not resume paused transfer progress after dismissing cancel popup 2020-10-19 10:25:52 +02:00
4893931a8d Fix deadlock in integration tests for Import-Export 2020-10-16 10:53:44 +02:00
932928ddc8 Allow to send calendar update multiple times 2020-10-15 13:11:40 +00:00
a33e414f01 Do not mix font awesome icon with regular text to avoid issues on Fedora 2020-10-15 12:48:09 +00:00
43d54c8f4f Clear separation of different message IDs in integration tests 2020-10-14 14:41:39 +02:00
6cbc11a75d Fix update on windows 2020-10-14 11:25:19 +02:00
a21bb130e1 Append duplicate of emails with References 2020-10-14 10:11:49 +02:00
12403785af fix: replace, don't add, transfer encoding when making body 7-bit clean 2020-10-09 13:55:37 +02:00
b4892855d4 Set flags by FLAGS (not using +/-FLAGS) do not change spam state 2020-10-06 08:42:33 +00:00
7ff67f2217 Reverted sending IMAP updates to be not blocking again 2020-10-05 11:33:16 +02:00
4912c27be8 Changelog 2020-10-05 10:51:11 +02:00
288ba11452 test: add test for sending pgp/mime as plaintext 2020-10-01 16:56:38 +02:00
7874183052 fix(GODT-770): handle extraneous end-of-mail 2020-10-01 16:16:15 +02:00
dc9851f8ea fix(GODT-749): don't force pgp/inline when sending plaintext 2020-10-01 10:47:39 +02:00
68616e470c chore: bump crypto version 2020-09-25 15:45:29 +02:00
53cd2ff524 CI artifacts only for a day 2020-09-25 11:29:45 +02:00
190 changed files with 5407 additions and 2007 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
@ -82,7 +88,11 @@ dependency-updates:
script: script:
- make build - make build
artifacts: artifacts:
expire_in: 2 week # 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.
expire_in: 1 day
tags:
- large
build-linux: build-linux:
extends: .build-base extends: .build-base

View File

@ -13,13 +13,21 @@ 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=
``` ```
### Build Bridge ### Build Bridge
* in project root run * in project root run
@ -44,6 +52,12 @@ make build-ie
* for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`) * for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`)
* for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`) * for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`)
### Tags
Note that repository contains both Bridge and Import-Export apps and they are
not released together. Therefore, each app has own tag prefix. Bridge tags
starts with `br-` and Import-Export tags starts with `ie-`. Both tags continue
with semantic versioning `MAJOR.MINOR.PATCH`. An example of full tag is
`br-1.4.4` or `ie-1.1.2` (current versions in October 2020).
## Useful tests, lints and checks ## Useful tests, lints and checks
In order to be able to run following commands please install the development dependencies: In order to be able to run following commands please install the development dependencies:

View File

@ -1,8 +1,135 @@
# ProtonMail Bridge Changelog # ProtonMail Bridge and Import-Export app Changelog
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Unreleased ## [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
* GODT-799 Skipped messages do not change total counts but shows as separate number.
### Fixed
* GODT-799 Fix skipping unwanted folders importing from mbox files.
* GODT-769 Close connection before deleting labels to prevent panics accessing deleted bucket.
### Removed
* GODT-766 Remove GUI popup for IMAP TLS error.
## [Bridge 1.5.0] Golden Gate
### Changed
* Updated go-mbox dependency back to upstream.
### Fixed
* GODT-847 Waiting for unilateral update during deleting the message.
* 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-685 Improve deb packaging regarding dejavu font.
## [IE 1.2.0] Elbe
### Added
* GODT-763 Detect Gmail labels from All Mail mbox export (using X-Gmail-Label header).
* GODT-834 Info about tags in BUILDS.md and link to Import-Export page in README.md.
* GODT-777 Support Apple Mail MBOX export format.
### Fixed
* GODT-677 Windows IE: global import settings not fit in window.
* GODT-794 Congo fails to update to Danube.
* GODT-749 Don't force PGP/Inline when sending plaintext messages.
* GODT-764 Fix deadlock in integration tests for Import-Export.
* GODT-662 Do not resume paused transfer progress after dismissing cancel popup.
* GODT-772 Sanitize mailbox names for exporting to follow OS restrictions.
* GODT-771 Show fatal errors after export is terminated.
* GODT-779 Do not propagate updates when progress is stopped.
* GODT-779 Unpause progress during fatal error to properly stop progress.
* GODT-779 Stop ongoing transfer calls sooner (re-check after import request is generated).
* Fix measurement of uploading attachments during transfer.
* GODT-827 Do not spam sentry with bad ID by integration test.
* GODT-700 Fix UTF-7 incompatibility.
* GODT-837 Fix flaky TestFailUnpauseAndStops.
* GODT-782 Don't use TLS pinning when checking connectivity status.
### Changed
* TLS pins conform to official list.
## [Bridge 1.4.5] Forth
### Fixed
* GODT-829 Remove `NoInferior` to display sub-folders in apple mail.
## [Bridge 1.4.4] Forth
### Fixed
* GODT-798 Replace, don't add, transfer encoding when making body 7-bit clean.
* Move/Copy duplicate for emails with References in Outlook.
* CSB-247 Cannot update from 1.4.0.
## [Bridge 1.4.3] Forth
### Changed
* Reverted sending IMAP updates to be not blocking again.
### Fixed
* GODT-783 Settings flags by FLAGS (not using +/-FLAGS) do not change spam state.
## [Bridge 1.4.2] Forth ## [Bridge 1.4.2] Forth
@ -10,6 +137,22 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-761 Use label.Path instead of Name to partially support subfolders for webapp beta release. * GODT-761 Use label.Path instead of Name to partially support subfolders for webapp beta release.
* GODT-765 Improve speed of checking whether message is deleted. * GODT-765 Improve speed of checking whether message is deleted.
## [IE 1.1.2] Danube (beta 2020-09-xx)
### Fixed
* GODT-770 Better handling of extraneous end-of-mail indicator.
* GODT-776 Fix crash when IMAP client connects while account is logging in.
### Changed
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8.
* GODT-785 Clear separation of different message IDs in integration tests.
### Changed
* GODT-741 Import-Export shows "Unable to parse time" notice instead of zero time in error report window.
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8.
* GODT-374 Allow to send calendar update multiple times.
## [IE 1.1.1] Danube (beta 2020-09-xx) [Bridge 1.4.1] Forth (beta 2020-09-xx) ## [IE 1.1.1] Danube (beta 2020-09-xx) [Bridge 1.4.1] Forth (beta 2020-09-xx)
### Fixed ### Fixed
@ -17,11 +160,15 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-752 Parsing non-utf8 multipart/alternative message. * GODT-752 Parsing non-utf8 multipart/alternative message.
* GODT-752 Parsing message with duplicate charset parameter. * GODT-752 Parsing message with duplicate charset parameter.
## [IE 1.1.0] Danube ## [IE 1.1.0] Danube
### Fixed ### Fixed
* GODT-703 Import-Export showed always at least one total message. * GODT-703 Import-Export showed always at least one total message.
* GODT-738 Fix for mbox files with long lines. * GODT-738 Fix for mbox files with long lines.
### Fixed
* GODT-732 Do not mix font awesome icon with regular text to avoid issues on Fedora.
## [Bridge 1.4.0] Forth ## [Bridge 1.4.0] Forth
@ -55,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.
@ -168,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.
@ -177,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.
@ -186,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.
@ -209,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.
@ -264,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.
@ -315,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.
@ -331,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.
@ -340,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.
@ -355,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.
@ -370,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.
@ -400,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.
@ -510,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.
@ -542,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.
@ -556,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.
@ -577,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.
@ -589,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]`.
@ -648,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`.
@ -724,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()`.
@ -757,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.
@ -789,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.
@ -810,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
@ -849,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
@ -871,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.
@ -895,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.
@ -934,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.4.0-git BRIDGE_APP_VERSION?=1.5.4-git
IE_APP_VERSION?=1.1.0-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,30 +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
@ -78,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
@ -96,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}
@ -112,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
@ -137,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
@ -197,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 ./...
@ -265,7 +287,6 @@ run-ie-qt:
run-ie-nogui: run-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) run-nogui TARGET_CMD=Import-Export $(MAKE) run-nogui
clean-frontend-qt: clean-frontend-qt:
$(MAKE) -C internal/frontend/qt -f Makefile.local clean $(MAKE) -C internal/frontend/qt -f Makefile.local clean
clean-frontend-qt-ie: clean-frontend-qt-ie:

View File

@ -1,9 +1,9 @@
# ProtonMail Bridge and Import Export app # ProtonMail Bridge and Import Export app
Copyright (c) 2020 Proton Technologies AG Copyright (c) 2020 Proton Technologies AG
This repository holds the ProtonMail Bridge application. 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).
@ -35,6 +35,8 @@ configure transfer rules (match source and target mailboxes, set time
range limits and so on) and hit start. Once the transfer is complete, range limits and so on) and hit start. Once the transfer is complete,
check the results. check the results.
More details [on the public website](https://protonmail.com/import-export).
## Keychain ## Keychain
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
Windows, Bridge uses native credential managers. On Linux, use Windows, Bridge uses native credential managers. On Linux, use

16
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,13 +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.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
@ -35,14 +34,15 @@ require (
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
github.com/emersion/go-mbox v1.0.0 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
@ -59,7 +59,7 @@ require (
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.6.0 github.com/sirupsen/logrus v1.7.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
@ -74,9 +74,7 @@ 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-20200828124548-d04b0dc1f399 github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474
github.com/emersion/go-mbox => github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45
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-20200416114516-1fa7f403fb9c golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8
) )

181
go.sum
View File

@ -1,13 +1,16 @@
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/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 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=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU= github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8 h1:u1j0xLTrCHpNS40B6m4Sv3IVUz5m9jt+AnTIopT3IgM=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig= github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
@ -16,36 +19,44 @@ github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg= github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399 h1:wBo/Xgb/Dn2loU47D+PJaOoIZ67i3AqYp51gLn8YE5U= github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474 h1:D0RwDtkBw0Gt7hmbb1ivdEulplJAwu1i2jzh4HM45fo=
github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-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-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE= github.com/ProtonMail/go-rfc5322 v0.2.1 h1:J2PHusboDAYUfE+uBfoJnKZPbnVmzK1zXw6dQrgV8yE=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI= github.com/ProtonMail/go-rfc5322 v0.2.1/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
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/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45 h1:GDh55hDI2sNiirDqEWV8b6EB729u78Qxu3nKF970n6g=
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
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/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA= github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
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=
@ -56,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=
@ -68,93 +84,175 @@ github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 h1:z8T
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4= github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.0 h1:HN6aKbyqmgIfK9fS/gen+NRr2wXLSxZXWfdAIAnzQPc= github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
github.com/emersion/go-mbox v1.0.0/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI= github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a h1:3C6qIGgPr1qAT0ikRD5NbyKpME/iHCDeXhpv/JJsFsE= github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a h1:3C6qIGgPr1qAT0ikRD5NbyKpME/iHCDeXhpv/JJsFsE=
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1:kYIioST9GDHte9/BRWgi93rpqbDuFftMjKSMaXS8ABo= github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1:kYIioST9GDHte9/BRWgi93rpqbDuFftMjKSMaXS8ABo=
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/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= 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/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 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=
@ -171,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=
@ -204,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=
@ -213,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 Tue Sep 29 14:56:25 CEST 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/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;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,21 +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 'Mon Sep 21 01:29:10 PM CEST 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 = `Bulletproofing against any potential data loss and/or duplication const ReleaseNotes = `Support read confirmations
Performance improvements for handling attachments and non-standard formatting Adding GPLv3 licence button to the GUI
Better stability of the message parser Improved testing
• Additional foreign encoding support for outgoing messages
• Complete refactor of the way messages are parsed to simplify code maintenance
• Improved User-Agent detection
• Added MacOS Big Sur compatibility
• Added persistent anonymous API cookies
` `
const ReleaseFixedBugs = `Fixed rare mail loss when moving from Spam folder const ReleaseFixedBugs = `AppleMail crashes (timestamp related)
Limited log size Encoding errors
Fixed Linux font issues (mouse hover). 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

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

View File

@ -184,9 +184,17 @@ func (f *frontendCLI) setTransferRules(t *transfer.Transfer) bool {
} }
func (f *frontendCLI) printTransferProgress(progress *transfer.Progress) { func (f *frontendCLI) printTransferProgress(progress *transfer.Progress) {
failed, imported, exported, added, total := progress.GetCounts() counts := progress.GetCounts()
if total != 0 { if counts.Total != 0 {
f.Println(fmt.Sprintf("Progress update: %d (%d / %d) / %d, failed: %d", imported, exported, added, total, failed)) f.Println(fmt.Sprintf(
"Progress update: %d (%d / %d) / %d, skipped: %d, failed: %d",
counts.Imported,
counts.Exported,
counts.Added,
counts.Total,
counts.Skipped,
counts.Failed,
))
} }
if progress.IsPaused() { if progress.IsPaused() {

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

@ -48,6 +48,7 @@ Item {
text : qsTr("Clear", "clickable link next to clear cache button in settings") text : qsTr("Clear", "clickable link next to clear cache button in settings")
color: Style.main.text color: Style.main.text
font { font {
family : cacheClear.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt pointSize : Style.settings.fontSize * Style.pt
underline : true underline : true
} }
@ -66,6 +67,7 @@ Item {
text : qsTr("Clear", "clickable link next to clear keychain button in settings") text : qsTr("Clear", "clickable link next to clear keychain button in settings")
color: Style.main.text color: Style.main.text
font { font {
family : cacheKeychain.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt pointSize : Style.settings.fontSize * Style.pt
underline : true underline : true
} }
@ -125,6 +127,7 @@ Item {
text : qsTr("Change", "clickable link next to change ports button in settings") text : qsTr("Change", "clickable link next to change ports button in settings")
color: Style.main.text color: Style.main.text
font { font {
family : changePort.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt pointSize : Style.settings.fontSize * Style.pt
underline : true underline : true
} }

View File

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

View File

@ -276,6 +276,10 @@ Item {
winMain.dialogExport.hide() winMain.dialogExport.hide()
} }
} }
onUpdateFinished : {
winMain.dialogUpdate.finished(hasError)
}
} }
function folderIcon(folderName, folderType) { // translations function folderIcon(folderName, folderType) { // translations

View File

@ -217,7 +217,10 @@ Dialog {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: { text: {
if (progressbarExport.isFinished) return qsTr("Export finished","todo") if (progressbarExport.isFinished) {
if (go.progressDescription=="") return qsTr("Export finished","todo")
else return qsTr("Export failed: %1").arg(go.progressDescription)
}
if ( if (
go.progressDescription == gui.enums.progressInit || go.progressDescription == gui.enums.progressInit ||
(go.progress==0 && go.description=="") (go.progress==0 && go.description=="")
@ -450,7 +453,6 @@ Dialog {
errorPopup.hide() errorPopup.hide()
} }
onClickedNo : { onClickedNo : {
go.resumeProcess()
errorPopup.hide() errorPopup.hide()
} }
} }

View File

@ -279,9 +279,8 @@ Dialog {
titleTo : root.address titleTo : root.address
} }
Rectangle { Column {
id: masterImportSettings id: masterImportSettings
height: 150 // fixme
anchors { anchors {
right : parent.right right : parent.right
left : parent.left left : parent.left
@ -291,45 +290,47 @@ Dialog {
rightMargin : Style.main.leftMargin rightMargin : Style.main.leftMargin
bottomMargin : Style.main.bottomMargin bottomMargin : Style.main.bottomMargin
} }
color: Style.dialog.background
Text { spacing: Style.main.bottomMargin
id: labelMasterImportSettings
text: qsTr("Master import settings:")
font { Row {
bold: true spacing: masterImportSettings.width - labelMasterImportSettings.width - resetSourceButton.width
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
color: Style.main.text
InfoToolTip { Text {
info: qsTr( id: labelMasterImportSettings
"If master import date range is selected only emails within this range will be imported, unless it is specified differently in folder date range.", text: qsTr("Master import settings:")
"Text in master import settings tooltip."
) font {
anchors { bold: true
left: parent.right family: Style.fontawesome.name
bottom: parent.bottom pointSize: Style.main.fontSize * Style.pt
leftMargin : Style.dialog.leftMargin }
color: Style.main.text
InfoToolTip {
anchors {
left: parent.right
bottom: parent.bottom
leftMargin : Style.dialog.leftMargin
}
info: qsTr(
"If master import date range is selected only emails within this range will be imported, unless it is specified differently in folder date range.",
"Text in master import settings tooltip."
)
} }
} }
}
// Reset all to default // Reset all to default
ClickIconText { ClickIconText {
anchors { id: resetSourceButton
right: parent.right text:qsTr("Reset all settings to default")
bottom: labelMasterImportSettings.bottom iconText: Style.fa.refresh
} textColor: Style.main.textBlue
text:qsTr("Reset all settings to default") onClicked: {
iconText: Style.fa.refresh go.resetSource()
textColor: Style.main.textBlue root.decrementCurrentIndex()
onClicked: { timer.start()
go.resetSource() }
root.decrementCurrentIndex()
timer.start()
} }
} }
@ -348,49 +349,59 @@ Dialog {
InlineDateRange { InlineDateRange {
id: globalDateRange id: globalDateRange
anchors {
left : parent.left
top : line.bottom
topMargin : Style.dialog.topMargin
}
} }
// Add global label (inline) // Add global label (inline)
InlineLabelSelect { InlineLabelSelect {
id: globalLabels id: globalLabels
anchors {
left : parent.left
top : globalDateRange.bottom
topMargin : Style.dialog.topMargin
}
//labelWidth : globalDateRange.labelWidth
} }
// Buttons
Row { Row {
spacing: Style.dialog.spacing spacing: Style.dialog.spacing
anchors{ CheckBoxLabel {
bottom : parent.bottom id: importEncrypted
right : parent.right text: qsTr("Import encrypted emails as they are")
anchors {
bottom: parent.bottom
bottomMargin: Style.dialog.fontSize/1.8
}
} }
ButtonRounded { InfoToolTip {
id: buttonCancelThree anchors {
fa_icon : Style.fa.times verticalCenter: importEncrypted.verticalCenter
text : qsTr("Cancel", "todo") }
color_main : Style.dialog.textBlue info: qsTr("When this option is enabled, encrypted emails will be imported as ciphertext. Otherwise, such messages will be skipped.", "todo")
onClicked : root.cancel()
} }
}
}
ButtonRounded { // Buttons
id: buttonNextThree Row {
fa_icon : Style.fa.check spacing: Style.dialog.spacing
text : qsTr("Import", "todo") anchors {
color_main : Style.dialog.background right: parent.right
color_minor : Style.dialog.textBlue bottom: parent.bottom
isOpaque : true rightMargin: Style.main.leftMargin
onClicked : root.okay() bottomMargin: Style.main.bottomMargin
} }
ButtonRounded {
id: buttonCancelThree
fa_icon : Style.fa.times
text : qsTr("Cancel", "todo")
color_main : Style.dialog.textBlue
onClicked : root.cancel()
}
ButtonRounded {
id: buttonNextThree
fa_icon : Style.fa.check
text : qsTr("Import", "todo")
color_main : Style.dialog.background
color_minor : Style.dialog.textBlue
isOpaque : true
onClicked : root.okay()
} }
} }
} }
@ -483,18 +494,30 @@ Dialog {
} }
} }
Text { Row {
property int fails: go.progressFails property int fails: go.progressFails
visible: fails > 0 visible: fails > 0
color : Style.main.textRed
font.family: Style.fontawesome.name
font.pointSize: Style.main.fontSize * Style.pt
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: Style.fa.exclamation_circle + " " + (
fails == 1 ? Text {
qsTr("%1 message failed to be imported").arg(fails) : color: Style.main.textRed
qsTr("%1 messages failed to be imported").arg(fails) font {
) pointSize : Style.dialog.fontSize * Style.pt
family : Style.fontawesome.name
}
text: Style.fa.exclamation_circle
}
Text {
property int fails: go.progressFails
color: Style.main.textRed
font.pointSize: Style.main.fontSize * Style.pt
text: " " + (
fails == 1 ?
qsTr("%1 message failed to be imported").arg(fails) :
qsTr("%1 messages failed to be imported").arg(fails)
)
}
} }
Row { // buttons Row { // buttons
@ -575,16 +598,27 @@ Dialog {
anchors.centerIn : finalReport anchors.centerIn : finalReport
spacing : Style.dialog.heightSeparator spacing : Style.dialog.heightSeparator
Text { Row {
text: go.progressDescription!="" ? qsTr("Import failed: %1").arg(go.progressDescription) : Style.fa.check_circle + " " + qsTr("Import completed successfully")
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen
font.bold : true Text {
font.family: Style.fontawesome.name font {
pointSize: Style.dialog.fontSize * Style.pt
family: Style.fontawesome.name
}
color: Style.main.textGreen
text: go.progressDescription!="" ? "" : Style.fa.check_circle
}
Text {
text: go.progressDescription!="" ? qsTr("Import failed: %1").arg(go.progressDescription) : " " + qsTr("Import completed successfully")
color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen
font.bold : true
}
} }
Text { Text {
text: qsTr("<b>Import summary:</b><br>Total number of emails: %1<br>Imported emails: %2<br>Errors: %3").arg(go.total).arg(finalReport.imported).arg(go.progressFails) text: qsTr("<b>Import summary:</b><br>Total number of emails: %1<br>Imported emails: %2<br>Filtered out emails: %3<br>Errors: %4").arg(go.total).arg(go.progressImported).arg(go.progressSkipped).arg(go.progressFails)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
textFormat: Text.RichText textFormat: Text.RichText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@ -773,11 +807,6 @@ Dialog {
errorPopup.hide() errorPopup.hide()
} }
onClickedNo : { onClickedNo : {
if (errorPopup.msgID == "ask_send_report") {
errorPopup.hide()
return
}
go.resumeProcess()
errorPopup.hide() errorPopup.hide()
} }
@ -1008,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

@ -74,7 +74,7 @@ Item {
) )
width: wrapper.width width: wrapper.width
color : Style.transparent color : Style.transparent
Text { AccessibleText {
id: aboutText id: aboutText
anchors { anchors {
bottom: parent.bottom bottom: parent.bottom
@ -82,8 +82,8 @@ Item {
} }
color: Style.main.textDisabled color: Style.main.textDisabled
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
font.family : Style.fontawesome.name font.pointSize : Style.main.fontSize * Style.pt
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n"+Style.fa.copyright + " 2020 Proton Technologies AG" text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n© 2020 Proton Technologies AG"
} }
} }
@ -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

@ -42,34 +42,50 @@ ComboBox {
root.below = popup.y>0 root.below = popup.y>0
} }
contentItem : Text { contentItem : Row {
id: boxText id: boxText
verticalAlignment: Text.AlignVCenter
font {
family: Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
bold: root.down
}
elide: Text.ElideRight
textFormat: Text.StyledText
text : root.displayText Text {
color: !root.enabled ? Style.main.textDisabled : ( root.down ? Style.main.background : Style.main.text ) anchors.verticalCenter: parent.verticalCenter
font {
pointSize: Style.dialog.fontSize * Style.pt
family: Style.fontawesome.name
}
text: {
if (view.currentIndex >= 0) {
if (!root.isFolderType) {
return Style.fa.tags + " "
}
var tgtIcon = view.currentItem.folderIcon
var tgtColor = view.currentItem.folderColor
if (tgtIcon != Style.fa.folder_open) {
return tgtIcon + " "
}
return '<font color="'+tgtColor+'">'+ tgtIcon + "</font> "
}
return ""
}
color: !root.enabled ? Style.main.textDisabled : ( root.down ? Style.main.background : Style.main.text )
}
Text {
anchors.verticalCenter: parent.verticalCenter
font {
pointSize : Style.dialog.fontSize * Style.pt
bold: root.down
}
elide: Text.ElideRight
textFormat: Text.StyledText
text : root.displayText
color: !root.enabled ? Style.main.textDisabled : ( root.down ? Style.main.background : Style.main.text )
}
} }
displayText: { displayText: {
if (view.currentIndex >= 0) { if (view.currentIndex >= 0) {
if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels") if (!root.isFolderType) return qsTr("Add/Remove labels")
return view.currentItem.folderName
var tgtName = view.currentItem.folderName
var tgtIcon = view.currentItem.folderIcon
var tgtColor = view.currentItem.folderColor
if (tgtIcon != Style.fa.folder_open) {
return tgtIcon + " " + tgtName
}
return '<font color="'+tgtColor+'">'+ tgtIcon + "</font> " + tgtName
} }
if (root.isFolderType) return qsTr("No folder selected") if (root.isFolderType) return qsTr("No folder selected")
return qsTr("No labels selected") return qsTr("No labels selected")

View File

@ -44,6 +44,7 @@ Item {
text : qsTr("Clear") text : qsTr("Clear")
color: Style.main.text color: Style.main.text
font { font {
family : cacheKeychain.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt pointSize : Style.settings.fontSize * Style.pt
underline : true underline : true
} }

View File

@ -84,7 +84,7 @@ Window {
height: content.height - ( height: content.height - (
(clientVersion.visible ? clientVersion.height + Style.dialog.fontSize : 0) + (clientVersion.visible ? clientVersion.height + Style.dialog.fontSize : 0) +
userAddress.height + Style.dialog.fontSize + userAddress.height + Style.dialog.fontSize +
securityNote.contentHeight + Style.dialog.fontSize + securityNoteText.contentHeight + Style.dialog.fontSize +
cancelButton.height + Style.dialog.fontSize cancelButton.height + Style.dialog.fontSize
) )
clip: true clip: true
@ -215,7 +215,7 @@ Window {
} }
// Note // Note
AccessibleText { Row {
id: securityNote id: securityNote
anchors { anchors {
left: parent.left left: parent.left
@ -223,14 +223,32 @@ Window {
top: userAddress.bottom top: userAddress.bottom
topMargin: Style.dialog.fontSize topMargin: Style.dialog.fontSize
} }
wrapMode: Text.Wrap
color: Style.dialog.text Text {
font.pointSize : Style.dialog.fontSize * Style.pt id: securityNoteIcon
text: font {
"<span style='font-family: " + Style.fontawesome.name + "'>" + Style.fa.exclamation_triangle + "</span> " + pointSize : Style.dialog.fontSize * Style.pt
qsTr("Bug reports are not end-to-end encrypted!", "The first part of warning in bug report form") + " " + family : Style.fontawesome.name
qsTr("Please do not send any sensitive information.", "The second part of warning in bug report form") + " " + }
qsTr("Contact us at security@protonmail.com for critical security issues.", "The third part of warning in bug report form") color: Style.dialog.text
text : Style.fa.exclamation_triangle
}
AccessibleText {
id: securityNoteText
anchors {
left: securityNoteIcon.right
leftMargin: 5 * Style.pt
right: parent.right
}
wrapMode: Text.Wrap
color: Style.dialog.text
font.pointSize : Style.dialog.fontSize * Style.pt
text:
qsTr("Bug reports are not end-to-end encrypted!", "The first part of warning in bug report form") + " " +
qsTr("Please do not send any sensitive information.", "The second part of warning in bug report form") + " " +
qsTr("Contact us at security@protonmail.com for critical security issues.", "The third part of warning in bug report form")
}
} }
// buttons // buttons

View File

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

View File

@ -840,6 +840,8 @@ Window {
property real progress: 0.0 property real progress: 0.0
property int progressFails: 0 property int progressFails: 0
property int progressImported: 0
property int progressSkipped: 0
property string progressDescription: "nothing" property string progressDescription: "nothing"
property string progressInit: "init" property string progressInit: "init"
property int total: 42 property int total: 42
@ -1011,6 +1013,8 @@ Window {
property SequentialAnimation animateProgressBar : SequentialAnimation { property SequentialAnimation animateProgressBar : SequentialAnimation {
id: apb id: apb
property real speedup : 1.0; property real speedup : 1.0;
PropertyAnimation{ target: go; properties: "progressSkipped"; to: 0; duration: 1; }
PropertyAnimation{ target: go; properties: "progressImported"; to: 0; duration: 1; }
PropertyAnimation{ target: go; properties: "importLogFileName"; to: ""; duration: 1; } PropertyAnimation{ target: go; properties: "importLogFileName"; to: ""; duration: 1; }
PropertyAnimation{ target: go; properties: "progressDescription"; to: go.progressInit; duration: 1; } PropertyAnimation{ target: go; properties: "progressDescription"; to: go.progressInit; duration: 1; }
PropertyAnimation{ duration: 2000/apb.speedup; } PropertyAnimation{ duration: 2000/apb.speedup; }
@ -1024,6 +1028,8 @@ Window {
PropertyAnimation{ target: go; properties: "progress"; to: 0.01; duration: 1; } PropertyAnimation{ target: go; properties: "progress"; to: 0.01; duration: 1; }
PropertyAnimation{ duration: 1000/apb.speedup; } PropertyAnimation{ duration: 1000/apb.speedup; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.1; duration: 1; } PropertyAnimation{ target: go; properties: "progress"; to: 0.1; duration: 1; }
PropertyAnimation{ target: go; properties: "progressSkipped"; to: 12; duration: 1; }
PropertyAnimation{ target: go; properties: "progressImported"; to: 13.1; duration: 1; }
PropertyAnimation{ duration: 1000/apb.speedup; } PropertyAnimation{ duration: 1000/apb.speedup; }
PropertyAnimation{ target: go; properties: "progress"; to: 0.3; duration: 1; } PropertyAnimation{ target: go; properties: "progress"; to: 0.3; duration: 1; }
PropertyAnimation{ target: go; properties: "progressFails"; to: 1; duration: 1; } PropertyAnimation{ target: go; properties: "progressFails"; to: 1; duration: 1; }

View File

@ -21,6 +21,7 @@ package qtcommon
import ( import (
"fmt" "fmt"
"github.com/therecipe/qt/core" "github.com/therecipe/qt/core"
) )

View File

@ -72,6 +72,9 @@ func (e *ErrorListModel) data(index *core.QModelIndex, role int) *core.QVariant
case MailSubject: case MailSubject:
return qtcommon.NewQVariantString(r.Subject) return qtcommon.NewQVariantString(r.Subject)
case MailDate: case MailDate:
if r.Time.IsZero() {
return qtcommon.NewQVariantString("Unavailable")
}
return qtcommon.NewQVariantString(r.Time.String()) return qtcommon.NewQVariantString(r.Time.String())
case MailFrom: case MailFrom:
return qtcommon.NewQVariantString(r.From) return qtcommon.NewQVariantString(r.From)

View File

@ -337,18 +337,25 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
if progress.IsStopped() { if progress.IsStopped() {
break break
} }
failed, imported, _, _, total := progress.GetCounts() counts := progress.GetCounts()
f.Qml.SetTotal(int(total)) f.Qml.SetTotal(int(counts.Total))
f.Qml.SetProgressFails(int(failed)) f.Qml.SetProgressImported(int(counts.Imported))
f.Qml.SetProgressSkipped(int(counts.Skipped))
f.Qml.SetProgressFails(int(counts.Failed))
f.Qml.SetProgressDescription(progress.PauseReason()) f.Qml.SetProgressDescription(progress.PauseReason())
if total > 0 { if counts.Total > 0 {
newProgress := float32(imported+failed) / float32(total) newProgress := counts.Progress()
if newProgress >= 0 && newProgress != f.Qml.Progress() { if newProgress >= 0 && newProgress != f.Qml.Progress() {
f.Qml.SetProgress(newProgress) f.Qml.SetProgress(newProgress)
f.Qml.ProgressChanged(newProgress) f.Qml.ProgressChanged(newProgress)
} }
} }
} }
// Counts will add lost messages only once the progress is completeled.
counts := progress.GetCounts()
f.Qml.SetProgressImported(int(counts.Imported))
f.Qml.SetProgressSkipped(int(counts.Skipped))
f.Qml.SetProgressFails(int(counts.Failed))
if err := progress.GetFatalError(); err != nil { if err := progress.GetFatalError(); err != nil {
f.Qml.SetProgressDescription(err.Error()) f.Qml.SetProgressDescription(err.Error())
@ -426,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,15 +73,18 @@ 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
f.Qml.SetProgressImported(0)
f.Qml.SetProgressSkipped(0)
f.Qml.SetProgressFails(0) f.Qml.SetProgressFails(0)
f.Qml.SetProgress(0.0) f.Qml.SetProgress(0.0)
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

@ -43,6 +43,8 @@ type GoQMLInterface struct {
_ string `property:lastError` _ string `property:lastError`
_ float32 `property:progress` _ float32 `property:progress`
_ string `property:progressDescription` _ string `property:progressDescription`
_ int `property:progressImported`
_ int `property:progressSkipped`
_ int `property:progressFails` _ int `property:progressFails`
_ int `property:total` _ int `property:total`
_ string `property:importLogFileName` _ string `property:importLogFileName`
@ -71,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"`
@ -93,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"`
@ -165,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

@ -40,17 +40,17 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig" "github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt-common" qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updates" "github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus" "github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/kardianos/osext" "github.com/kardianos/osext"
"github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
"github.com/therecipe/qt/core" "github.com/therecipe/qt/core"
"github.com/therecipe/qt/gui" "github.com/therecipe/qt/gui"
@ -187,7 +187,6 @@ func (s *FrontendQt) watchEvents() {
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent) updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent)
newUserCh := s.getEventChannel(events.UserRefreshEvent) newUserCh := s.getEventChannel(events.UserRefreshEvent)
certIssue := s.getEventChannel(events.TLSCertIssue) certIssue := s.getEventChannel(events.TLSCertIssue)
imapCertIssue := s.getEventChannel(events.IMAPTLSBadCert)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -227,8 +226,6 @@ func (s *FrontendQt) watchEvents() {
s.Qml.LoadAccounts() s.Qml.LoadAccounts()
case <-certIssue: case <-certIssue:
s.Qml.ShowCertIssue() s.Qml.ShowCertIssue()
case <-imapCertIssue:
s.Qml.ShowIMAPCertTroubleshoot()
} }
} }
} }
@ -418,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"`
@ -135,7 +136,6 @@ type GoQMLInterface struct {
_ func(x, y float32) `slot:"saveOutgoingNoEncPopupCoord"` _ func(x, y float32) `slot:"saveOutgoingNoEncPopupCoord"`
_ func(recipient string) `signal:"showNoActiveKeyForRecipient"` _ func(recipient string) `signal:"showNoActiveKeyForRecipient"`
_ func() `signal:"showCertIssue"` _ func() `signal:"showCertIssue"`
_ func() `signal:"ShowIMAPCertTroubleshoot"`
_ func() `slot:"startUpdate"` _ func() `slot:"startUpdate"`
_ func(hasError bool) `signal:"updateFinished"` _ func(hasError bool) `signal:"updateFinished"`
@ -153,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

@ -28,7 +28,6 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
goIMAPBackend "github.com/emersion/go-imap/backend" goIMAPBackend "github.com/emersion/go-imap/backend"
"github.com/sirupsen/logrus"
) )
type panicHandler interface { type panicHandler interface {
@ -47,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.
@ -59,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
@ -85,6 +83,9 @@ func newIMAPBackend(
imapCachePath: cfg.GetIMAPCachePath(), imapCachePath: cfg.GetIMAPCachePath(),
imapCacheLock: &sync.RWMutex{}, imapCacheLock: &sync.RWMutex{},
updatesBlocking: map[string]bool{},
updatesBlockingLocker: &sync.Mutex{},
} }
} }
@ -170,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
} }
@ -198,11 +201,3 @@ func (ib *imapBackend) monitorDisconnectedUsers() {
ib.deleteUser(address) ib.deleteUser(address)
} }
} }
func (ib *imapBackend) upgradeError(err error) {
logrus.WithError(err).Error("IMAP connection couldn't be upgraded to TLS during STARTTLS")
if strings.Contains(err.Error(), "remote error: tls: bad certificate") {
ib.eventListener.Emit(events.IMAPTLSBadCert, err.Error())
}
}

View File

@ -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)
@ -76,5 +75,9 @@ func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
} }
func (u *bridgeUserWrap) GetStore() storeUserProvider { func (u *bridgeUserWrap) GetStore() storeUserProvider {
return newStoreUserWrap(u.User.GetStore()) store := u.User.GetStore()
if store == nil {
return nil
}
return newStoreUserWrap(store)
} }

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

@ -80,7 +80,10 @@ func (im *imapMailbox) Info() (*imap.MailboxInfo, error) {
} }
func (im *imapMailbox) getFlags() []string { func (im *imapMailbox) getFlags() []string {
flags := []string{imap.NoInferiorsAttr} // Subfolders are not yet supported by API. flags := []string{}
if !im.storeMailbox.IsFolder() || im.storeMailbox.IsSystem() {
flags = append(flags, imap.NoInferiorsAttr) // Subfolders are not supported for System or Label
}
switch im.storeMailbox.LabelID() { switch im.storeMailbox.LabelID() {
case pmapi.SentLabel: case pmapi.SentLabel:
flags = append(flags, specialuse.Sent) flags = append(flags, specialuse.Sent)
@ -115,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),
@ -124,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{
@ -174,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

@ -24,7 +24,6 @@ import (
"mime/multipart" "mime/multipart"
"net/mail" "net/mail"
"net/textproto" "net/textproto"
"regexp"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -41,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() }
@ -141,18 +144,19 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
references := m.Header.Get("References") references := m.Header.Get("References")
referenceList := strings.Fields(references) referenceList := strings.Fields(references)
if len(referenceList) > 0 { // In case there is a mail client which corrupts headers, try
// "References" too.
if internalID == "" && len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1] lastReference := referenceList[len(referenceList)-1]
// In case we are using a mail client which corrupts headers, try "References" too. match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference)
re := regexp.MustCompile(pmapi.InternalReferenceFormat) if len(match) == 2 {
match := re.FindStringSubmatch(lastReference) internalID = match[1]
if len(match) > 0 {
internalID = match[0]
} }
} }
// Avoid appending a message which is already on the server. Apply the new // Avoid appending a message which is already on the server. Apply the
// label instead. This sometimes happens which Outlook (it uses APPEND instead of COPY). // new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY).
if internalID != "" { if internalID != "" {
// Check to see if this belongs to a different address in split mode or another ProtonMail account. // Check to see if this belongs to a different address in split mode or another ProtonMail account.
msg, err := im.storeMailbox.GetMessage(internalID) msg, err := im.storeMailbox.GetMessage(internalID)
@ -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
@ -57,6 +60,10 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
return im.addOrRemoveFlags(operation, messageIDs, flags) return im.addOrRemoveFlags(operation, messageIDs, flags)
} }
// setFlags is used for FLAGS command (not +FLAGS or -FLAGS), which means
// to set flags passed as an argument and unset the rest. For example,
// if message is not read, is flagged and is not deleted, call FLAGS \Seen
// should flag message as read, unflagged and keep undeleted.
func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
seen := false seen := false
flagged := false flagged := false
@ -106,16 +113,17 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
} }
} }
spamMailbox, err := im.storeAddress.GetMailbox("Spam") // Spam should not be taken into action here as Outlook is using FLAGS
if err != nil { // without preserving junk flag. Probably it's because junk is not standard
return err // in the rfc3501 and thus Outlook expects calling FLAGS \Seen will not
} // change the state of junk or other non-standard flags.
// Still, its safe to label as spam once any client sends the request.
if spam { if spam {
if err := spamMailbox.LabelMessages(messageIDs); err != nil { spamMailbox, err := im.storeAddress.GetMailbox("Spam")
if err != nil {
return err return err
} }
} else { if err := spamMailbox.LabelMessages(messageIDs); err != nil {
if err := spamMailbox.UnlabelMessages(messageIDs); err != nil {
return err return err
} }
} }

View File

@ -58,7 +58,6 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
s.AllowInsecureAuth = true s.AllowInsecureAuth = true
s.ErrorLog = newServerErrorLogger("server-imap") s.ErrorLog = newServerErrorLogger("server-imap")
s.AutoLogout = 30 * time.Minute s.AutoLogout = 30 * time.Minute
s.UpgradeError = imapBackend.upgradeError
serverID := imapid.ID{ serverID := imapid.ID{
imapid.FieldName: "ProtonMail Bridge", imapid.FieldName: "ProtonMail Bridge",
@ -127,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() {
@ -140,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 Tue Sep 29 14:56:25 CEST 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/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;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,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 Sep 23 01:31:53 PM CEST 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 = `Speed up import by implementing parallel processing (parallel fetch, encrypt and upload of messages) const ReleaseNotes = `Allow an import of already encrypted messages (as cypher text)
Optimising the initial fetch of messages from external accounts Cosmetic GUI changes
• Better handling of attachments and non-standard formatting • Better error handling
• Improved stability of the message parser
• Added persistent anonymous API cookies
` `
const ReleaseFixedBugs = `• Import from mbox files with long lines const ReleaseFixedBugs = `• Installation issues on linux
• Improvements to import from Yahoo accounts
` `

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
@ -503,13 +507,6 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
} }
func (b *sendPreferencesBuilder) setMIMEPreferences(composerMIMEType string) { func (b *sendPreferencesBuilder) setMIMEPreferences(composerMIMEType string) {
// If the sign flag (that we just determined above) is true we use the scheme
// in the encryption preferences, unless the plain text format has been
// selected in the composer, in which case we must enforce PGP/INLINE.
if !b.isInternal() && b.shouldSign() && composerMIMEType == "text/plain" {
b.withScheme(pgpInline)
}
// If the sign flag (that we just determined above) is true, then the MIME // If the sign flag (that we just determined above) is true, then the MIME
// type is determined by the PGP scheme (also determined above): we should // type is determined by the PGP scheme (also determined above): we should
// use 'text/plain' for a PGP/Inline scheme, and 'multipart/mixed' otherwise. // use 'text/plain' for a PGP/Inline scheme, and 'multipart/mixed' otherwise.

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,34 @@ 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",
contactMeta: &ContactMetadata{Sign: true, SignIsSet: true},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/plain"},
wantEncrypt: false,
wantSign: true,
wantScheme: pmapi.ClearMIMEPackage,
wantMIMEType: "multipart/mixed",
},
{ {
name: "external with pinned contact public key but no intention to encrypt/sign", name: "external with pinned contact public key but no intention to encrypt/sign",

View File

@ -20,6 +20,7 @@ package smtp
import ( import (
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"strings"
"sync" "sync"
"time" "time"
@ -48,6 +49,15 @@ func newSendRecorder() *sendRecorder {
} }
func (q *sendRecorder) getMessageHash(message *pmapi.Message) string { func (q *sendRecorder) getMessageHash(message *pmapi.Message) string {
// Outlook Calendar updates has only headers (no body) and thus have always
// the same hash. If the message is type of calendar, the "is sending"
// check to avoid potential duplicates is skipped. Duplicates should not
// be a problem in this case as calendar updates are small.
contentType := message.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "text/calendar") {
return ""
}
h := sha256.New() h := sha256.New()
_, _ = h.Write([]byte(message.AddressID + message.Subject)) _, _ = h.Write([]byte(message.AddressID + message.Subject))
if message.Sender != nil { if message.Sender != nil {
@ -101,6 +111,10 @@ func (q *sendRecorder) isSendingOrSent(client messageGetter, hash string) (isSen
q.lock.Lock() q.lock.Lock()
defer q.lock.Unlock() defer q.lock.Unlock()
if hash == "" {
return false, false
}
q.deleteExpiredKeys() q.deleteExpiredKeys()
value, ok := q.hashes[hash] value, ok := q.hashes[hash]
if !ok { if !ok {

View File

@ -349,6 +349,32 @@ func TestSendRecorder_getMessageHash(t *testing.T) {
}, },
false, false,
}, },
{ // Different content type - calendar
&pmapi.Message{
Header: mail.Header{
"Content-Type": []string{"text/calendar"},
},
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
} }
for i, tc := range testCases { for i, tc := range testCases {
tc := tc // bind tc := tc // bind
@ -382,12 +408,13 @@ func TestSendRecorder_isSendingOrSent(t *testing.T) {
{"hash", &pmapi.Message{Type: pmapi.MessageTypeDraft, Time: time.Now().Unix()}, nil, true, false}, {"hash", &pmapi.Message{Type: pmapi.MessageTypeDraft, Time: time.Now().Unix()}, nil, true, false},
{"hash", &pmapi.Message{Type: pmapi.MessageTypeSent}, nil, false, true}, {"hash", &pmapi.Message{Type: pmapi.MessageTypeSent}, nil, false, true},
{"hash", &pmapi.Message{Type: pmapi.MessageTypeInboxAndSent}, nil, false, true}, {"hash", &pmapi.Message{Type: pmapi.MessageTypeInboxAndSent}, nil, false, true},
{"", &pmapi.Message{Type: pmapi.MessageTypeInboxAndSent}, nil, false, false},
} }
for i, tc := range testCases { for i, tc := range testCases {
tc := tc // bind tc := tc // bind
t.Run(fmt.Sprintf("%d / %v / %v / %v", i, tc.hash, tc.message, tc.err), func(t *testing.T) { t.Run(fmt.Sprintf("%d / %v / %v / %v", i, tc.hash, tc.message, tc.err), func(t *testing.T) {
messageGetter := &testSendRecorderGetMessageMock{message: tc.message, err: tc.err} messageGetter := &testSendRecorderGetMessageMock{message: tc.message, err: tc.err}
isSending, wasSent := q.isSendingOrSent(messageGetter, "hash") isSending, wasSent := q.isSendingOrSent(messageGetter, tc.hash)
assert.Equal(t, tc.wantIsSending, isSending, "isSending does not match") assert.Equal(t, tc.wantIsSending, isSending, "isSending does not match")
assert.Equal(t, tc.wantWasSent, wasSent, "wasSent does not match") assert.Equal(t, tc.wantWasSent, wasSent, "wasSent does not match")
}) })

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

@ -25,7 +25,6 @@ import (
"io" "io"
"mime" "mime"
"net/mail" "net/mail"
"regexp"
"strings" "strings"
"time" "time"
@ -45,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.
@ -54,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")
@ -67,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
} }
@ -146,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.
@ -188,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, "<>")
@ -257,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
@ -267,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 {
@ -299,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
}
} }
} }
@ -371,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)
} }
@ -408,9 +377,9 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) { if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) {
newReferences = append(newReferences, reference) newReferences = append(newReferences, reference)
} else { // internalid is the parentID. } else { // internalid is the parentID.
idMatch := regexp.MustCompile(pmapi.InternalReferenceFormat).FindStringSubmatch(reference) idMatch := pmapi.RxInternalReferenceFormat.FindStringSubmatch(reference)
if len(idMatch) > 0 { if len(idMatch) == 2 {
lastID := strings.TrimSuffix(strings.Trim(idMatch[0], "<>"), "@protonmail.internalid") lastID := idMatch[1]
filter := &pmapi.MessagesFilter{ID: []string{lastID}} filter := &pmapi.MessagesFilter{ID: []string{lastID}}
if su.addressID != "" { if su.addressID != "" {
filter.AddressID = su.addressID filter.AddressID = su.addressID

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,126 +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) { // 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)
} }
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,
) {
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)
}
func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumber uint32) {
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)
}
func (store *Store) imapMailboxCreated(address, mailboxName string) {
store.log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
}).Trace("IDLE mailbox info")
update := new(imapBackend.MailboxInfoUpdate)
update.Update = imapBackend.NewUpdate(address, "")
update.MailboxInfo = &imap.MailboxInfo{
Attributes: []string{imap.NoInferiorsAttr},
Delimiter: PathDelimiter,
Name: mailboxName,
}
store.imapSendUpdate(update)
}
func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) {
store.log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
"total": total,
"unread": unread,
"unreadSeqNum": unreadSeqNum,
}).Trace("IDLE status")
update := new(imapBackend.MailboxUpdate)
update.Update = imapBackend.NewUpdate(address, mailboxName)
update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen})
update.MailboxStatus.Messages = uint32(total)
update.MailboxStatus.Unseen = uint32(unread)
update.MailboxStatus.UnseenSeqNum = uint32(unreadSeqNum)
store.imapSendUpdate(update)
}
func (store *Store) imapSendUpdate(update imapBackend.Update) {
if store.imapUpdates == nil {
store.log.Trace("IMAP IDLE unavailable")
return return
} }
store.notifier.Notice(address, notice)
}
done := update.Done() func (store *Store) notifyUpdateMessage(address, mailboxName string, uid, sequenceNumber uint32, msg *pmapi.Message, hasDeletedFlag bool) {
go func() { if store.notifier == nil {
// This timeout is to not keep running many blocked goroutines.
// In case nothing listens to this channel, this thread should stop.
select {
case store.imapUpdates <- update:
case <-time.After(1 * time.Second):
store.log.Warn("IMAP update could not be sent (timeout).")
}
}()
// This timeout is to not block IMAP backend by wait for IMAP client.
select {
case <-done:
case <-time.After(1 * time.Second):
store.log.Warn("IMAP update could not be delivered (timeout).")
return 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

@ -21,6 +21,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings" "strings"
"sync/atomic"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -38,6 +39,8 @@ type Mailbox struct {
color string color string
log *logrus.Entry log *logrus.Entry
isDeleting atomic.Value
} }
func newMailbox(storeAddress *Address, labelID, labelPrefix, labelName, color string) (mb *Mailbox, err error) { func newMailbox(storeAddress *Address, labelID, labelPrefix, labelName, color string) (mb *Mailbox, err error) {
@ -59,6 +62,7 @@ func txNewMailbox(tx *bolt.Tx, storeAddress *Address, labelID, labelPrefix, labe
color: color, color: color,
log: l, log: l,
} }
mb.isDeleting.Store(false)
err := initMailboxBucket(tx, mb.getBucketName()) err := initMailboxBucket(tx, mb.getBucketName())
if err != nil { if err != nil {
@ -215,6 +219,7 @@ func (storeMailbox *Mailbox) Rename(newName string) error {
// Deletion has to be propagated to all the same mailboxes in all addresses. // Deletion has to be propagated to all the same mailboxes in all addresses.
// The propagation is processed by the event loop. // The propagation is processed by the event loop.
func (storeMailbox *Mailbox) Delete() error { func (storeMailbox *Mailbox) Delete() error {
storeMailbox.isDeleting.Store(true)
return storeMailbox.storeAddress.deleteMailbox(storeMailbox.labelID) return storeMailbox.storeAddress.deleteMailbox(storeMailbox.labelID)
} }
@ -226,6 +231,14 @@ func (storeMailbox *Mailbox) GetDelimiter() string {
// deleteMailboxEvent deletes the mailbox bucket. // deleteMailboxEvent deletes the mailbox bucket.
// This is called from the event loop. // This is called from the event loop.
func (storeMailbox *Mailbox) deleteMailboxEvent() error { func (storeMailbox *Mailbox) deleteMailboxEvent() error {
if !storeMailbox.isDeleting.Load().(bool) {
// Deleting label removes bucket. Any ongoing connection selected
// in such mailbox then might panic because of non-existing bucket.
// Closing connetions prevents that panic but if the connection
// asked for deletion, it should not be closed so it can receive
// successful response.
storeMailbox.store.user.CloseAllConnections()
}
return storeMailbox.db().Update(func(tx *bolt.Tx) error { return storeMailbox.db().Update(func(tx *bolt.Tx) error {
return tx.Bucket(mailboxesBucket).DeleteBucket(storeMailbox.getBucketName()) return tx.Bucket(mailboxesBucket).DeleteBucket(storeMailbox.getBucketName())
}) })

View File

@ -231,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
} }
@ -278,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
} }
@ -354,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),
@ -388,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,
@ -439,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,
@ -457,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,
@ -501,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))
storeMailbox.store.imapUpdateMessage( storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address, storeMailbox.storeAddress.address,
storeMailbox.labelName, storeMailbox.labelName,
uid, uid,

View File

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/store (interfaces: PanicHandler,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
@ -105,6 +105,18 @@ func (m *MockBridgeUser) EXPECT() *MockBridgeUserMockRecorder {
return m.recorder return m.recorder
} }
// CloseAllConnections mocks base method
func (m *MockBridgeUser) CloseAllConnections() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "CloseAllConnections")
}
// CloseAllConnections indicates an expected call of CloseAllConnections
func (mr *MockBridgeUserMockRecorder) CloseAllConnections() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseAllConnections", reflect.TypeOf((*MockBridgeUser)(nil).CloseAllConnections))
}
// CloseConnection mocks base method // CloseConnection mocks base method
func (m *MockBridgeUser) CloseConnection(arg0 string) { func (m *MockBridgeUser) CloseConnection(arg0 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -229,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

@ -5,9 +5,10 @@
package mocks package mocks
import ( import (
gomock "github.com/golang/mock/gomock"
reflect "reflect" reflect "reflect"
time "time" time "time"
gomock "github.com/golang/mock/gomock"
) )
// MockListener is a mock of Listener interface // MockListener is a mock of Listener interface

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

@ -36,6 +36,7 @@ type BridgeUser interface {
GetPrimaryAddress() string GetPrimaryAddress() string
GetStoreAddresses() []string GetStoreAddresses() []string
UpdateUser() error UpdateUser() error
CloseAllConnections()
CloseConnection(string) CloseConnection(string)
Logout() error Logout() error
} }

View File

@ -29,18 +29,36 @@ type Message struct {
ID string ID string
Unread bool Unread bool
Body []byte Body []byte
Source Mailbox Sources []Mailbox
Targets []Mailbox Targets []Mailbox
} }
// sourceNames returns array of source mailbox names.
func (msg Message) sourceNames() (names []string) {
for _, mailbox := range msg.Sources {
names = append(names, mailbox.Name)
}
return
}
// targetNames returns array of target mailbox names.
func (msg Message) targetNames() (names []string) {
for _, mailbox := range msg.Targets {
names = append(names, mailbox.Name)
}
return
}
// MessageStatus holds status for message used by progress manager. // MessageStatus holds status for message used by progress manager.
type MessageStatus struct { type MessageStatus struct {
eventTime time.Time // Time of adding message to the process. eventTime time.Time // Time of adding message to the process.
rule *Rule // Rule with source and target mailboxes. sourceNames []string // Source mailbox names message is in.
SourceID string // Message ID at the source. SourceID string // Message ID at the source.
targetID string // Message ID at the target (if any). targetNames []string // Target mailbox names message is in.
bodyHash string // Hash of the message body. targetID string // Message ID at the target (if any).
bodyHash string // Hash of the message body.
skipped bool
exported bool exported bool
imported bool imported bool
exportErr error exportErr error
@ -79,7 +97,7 @@ func (status *MessageStatus) setDetailsFromHeader(header mail.Header) {
} }
func (status *MessageStatus) hasError(includeMissing bool) bool { func (status *MessageStatus) hasError(includeMissing bool) bool {
return status.exportErr != nil || status.importErr != nil || (includeMissing && !status.imported) return status.exportErr != nil || status.importErr != nil || (includeMissing && !status.skipped && !status.imported)
} }
// GetErrorMessage returns error message. // GetErrorMessage returns error message.
@ -88,6 +106,9 @@ func (status *MessageStatus) GetErrorMessage() string {
} }
func (status *MessageStatus) getErrorMessage(includeMissing bool) string { func (status *MessageStatus) getErrorMessage(includeMissing bool) string {
if status.skipped {
return ""
}
if status.exportErr != nil { if status.exportErr != nil {
return fmt.Sprintf("failed to export: %s", status.exportErr) return fmt.Sprintf("failed to export: %s", status.exportErr)
} }

View File

@ -1,13 +1,16 @@
// 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
import ( import (
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
reflect "reflect" reflect "reflect"
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"
) )
// MockPanicHandler is a mock of PanicHandler interface // MockPanicHandler is a mock of PanicHandler interface
@ -95,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

@ -93,7 +93,7 @@ func (p *Progress) fatal(err error) {
defer p.lock.Unlock() defer p.lock.Unlock()
log.WithError(err).Error("Progress finished") log.WithError(err).Error("Progress finished")
p.isStopped = true p.setStop()
p.fatalError = err p.fatalError = err
p.cleanUpdateCh() p.cleanUpdateCh()
} }
@ -126,19 +126,33 @@ func (p *Progress) updateCount(mailbox string, count uint) {
} }
// addMessage should be called as soon as there is ID of the message. // addMessage should be called as soon as there is ID of the message.
func (p *Progress) addMessage(messageID string, rule *Rule) { func (p *Progress) addMessage(messageID string, sourceNames, targetNames []string) {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
defer p.update() defer p.update()
p.log.WithField("id", messageID).Trace("Message added") p.log.WithField("id", messageID).Trace("Message added")
p.messageStatuses[messageID] = &MessageStatus{ p.messageStatuses[messageID] = &MessageStatus{
eventTime: time.Now(), eventTime: time.Now(),
rule: rule, sourceNames: sourceNames,
SourceID: messageID, SourceID: messageID,
targetNames: targetNames,
} }
} }
// messageSkipped should be called once the message is skipped due to some
// filter such as time or folder and so on.
func (p *Progress) messageSkipped(messageID string) {
p.lock.Lock()
defer p.lock.Unlock()
defer p.update()
p.log.WithField("id", messageID).Debug("Message skipped")
p.messageStatuses[messageID].skipped = true
p.logMessage(messageID)
}
// messageExported should be called right before message is exported. // messageExported should be called right before message is exported.
func (p *Progress) messageExported(messageID string, body []byte, err error) { func (p *Progress) messageExported(messageID string, body []byte, err error) {
p.lock.Lock() p.lock.Lock()
@ -282,6 +296,15 @@ func (p *Progress) Stop() {
defer p.update() defer p.update()
p.log.Info("Progress stopped") p.log.Info("Progress stopped")
p.setStop()
// Once progress is stopped, some calls might be in progress. Results from
// those calls are irrelevant so we can close update channel sooner to not
// propagate any progress to user interface anymore.
p.cleanUpdateCh()
}
func (p *Progress) setStop() {
p.isStopped = true p.isStopped = true
p.pauseReason = "" // Clear pause to run paused code and stop it. p.pauseReason = "" // Clear pause to run paused code and stop it.
} }
@ -320,35 +343,40 @@ func (p *Progress) GetFailedMessages() []*MessageStatus {
} }
// GetCounts returns counts of exported and imported messages. // GetCounts returns counts of exported and imported messages.
func (p *Progress) GetCounts() (failed, imported, exported, added, total uint) { func (p *Progress) GetCounts() ProgressCounts {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
counts := ProgressCounts{}
// Return counts only once total is estimated or the process already // Return counts only once total is estimated or the process already
// ended (for a case when it ended quickly to report it correctly). // ended (for a case when it ended quickly to report it correctly).
if p.updateCh != nil && !p.messageCounted { if p.updateCh != nil && !p.messageCounted {
return return counts
} }
// Include lost messages in the process only when transfer is done. // Include lost messages in the process only when transfer is done.
includeMissing := p.updateCh == nil includeMissing := p.updateCh == nil
for _, mailboxCount := range p.messageCounts { for _, mailboxCount := range p.messageCounts {
total += mailboxCount counts.Total += mailboxCount
} }
for _, status := range p.messageStatuses { for _, status := range p.messageStatuses {
added++ counts.Added++
if status.skipped {
counts.Skipped++
}
if status.exported { if status.exported {
exported++ counts.Exported++
} }
if status.imported { if status.imported {
imported++ counts.Imported++
} }
if status.hasError(includeMissing) { if status.hasError(includeMissing) {
failed++ counts.Failed++
} }
} }
return return counts
} }
// GenerateBugReport generates similar file to import log except private information. // GenerateBugReport generates similar file to import log except private information.

View File

@ -0,0 +1,35 @@
// 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
// ProgressCounts holds counts counted by Progress.
type ProgressCounts struct {
Failed,
Skipped,
Imported,
Exported,
Added,
Total uint
}
// Progress returns ratio between processed messages (fully imported, skipped
// and failed ones) and total number of messages as percentage (0 - 1).
func (c *ProgressCounts) Progress() float32 {
progressed := c.Imported + c.Skipped + c.Failed
return float32(progressed) / float32(c.Total)
}

View File

@ -19,6 +19,7 @@ package transfer
import ( import (
"testing" "testing"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
a "github.com/stretchr/testify/assert" a "github.com/stretchr/testify/assert"
@ -38,8 +39,8 @@ func TestProgressUpdateCount(t *testing.T) {
progress.finish() progress.finish()
_, _, _, _, total := progress.GetCounts() //nolint[dogsled] counts := progress.GetCounts()
r.Equal(t, uint(42), total) r.Equal(t, uint(42), counts.Total)
} }
func TestProgressAddingMessages(t *testing.T) { func TestProgressAddingMessages(t *testing.T) {
@ -47,31 +48,36 @@ func TestProgressAddingMessages(t *testing.T) {
drainProgressUpdateChannel(&progress) drainProgressUpdateChannel(&progress)
// msg1 has no problem. // msg1 has no problem.
progress.addMessage("msg1", nil) progress.addMessage("msg1", []string{}, []string{})
progress.messageExported("msg1", []byte(""), nil) progress.messageExported("msg1", []byte(""), nil)
progress.messageImported("msg1", "", nil) progress.messageImported("msg1", "", nil)
// msg2 has an import problem. // msg2 has an import problem.
progress.addMessage("msg2", nil) progress.addMessage("msg2", []string{}, []string{})
progress.messageExported("msg2", []byte(""), nil) progress.messageExported("msg2", []byte(""), nil)
progress.messageImported("msg2", "", errors.New("failed import")) progress.messageImported("msg2", "", errors.New("failed import"))
// msg3 has an export problem. // msg3 has an export problem.
progress.addMessage("msg3", nil) progress.addMessage("msg3", []string{}, []string{})
progress.messageExported("msg3", []byte(""), errors.New("failed export")) progress.messageExported("msg3", []byte(""), errors.New("failed export"))
// msg4 has an export problem and import is also called. // msg4 has an export problem and import is also called.
progress.addMessage("msg4", nil) progress.addMessage("msg4", []string{}, []string{})
progress.messageExported("msg4", []byte(""), errors.New("failed export")) progress.messageExported("msg4", []byte(""), errors.New("failed export"))
progress.messageImported("msg4", "", nil) progress.messageImported("msg4", "", nil)
// msg5 is skipped.
progress.addMessage("msg5", []string{}, []string{})
progress.messageSkipped("msg5")
progress.finish() progress.finish()
failed, imported, exported, added, _ := progress.GetCounts() counts := progress.GetCounts()
a.Equal(t, uint(4), added) a.Equal(t, uint(5), counts.Added)
a.Equal(t, uint(2), exported) a.Equal(t, uint(2), counts.Exported)
a.Equal(t, uint(2), imported) a.Equal(t, uint(2), counts.Imported)
a.Equal(t, uint(3), failed) a.Equal(t, uint(1), counts.Skipped)
a.Equal(t, uint(3), counts.Failed)
errorsMap := map[string]string{} errorsMap := map[string]string{}
for _, status := range progress.GetFailedMessages() { for _, status := range progress.GetFailedMessages() {
@ -91,7 +97,7 @@ func TestProgressFinish(t *testing.T) {
progress.finish() progress.finish()
r.Nil(t, progress.updateCh) r.Nil(t, progress.updateCh)
r.NotPanics(t, func() { progress.addMessage("msg", nil) }) r.NotPanics(t, func() { progress.addMessage("msg", []string{}, []string{}) })
} }
func TestProgressFatalError(t *testing.T) { func TestProgressFatalError(t *testing.T) {
@ -101,7 +107,29 @@ func TestProgressFatalError(t *testing.T) {
progress.fatal(errors.New("fatal error")) progress.fatal(errors.New("fatal error"))
r.Nil(t, progress.updateCh) r.Nil(t, progress.updateCh)
r.NotPanics(t, func() { progress.addMessage("msg", nil) }) r.NotPanics(t, func() { progress.addMessage("msg", []string{}, []string{}) })
}
func TestFailUnpauseAndStops(t *testing.T) {
progress := newProgress(log, nil)
drainProgressUpdateChannel(&progress)
progress.Pause("pausing")
progress.fatal(errors.New("fatal error"))
r.Nil(t, progress.updateCh)
r.True(t, progress.isStopped)
r.False(t, progress.IsPaused())
r.Eventually(t, progress.shouldStop, time.Second, 10*time.Millisecond)
}
func TestStopClosesUpdates(t *testing.T) {
progress := newProgress(log, nil)
ch := progress.updateCh
progress.Stop()
r.Nil(t, progress.updateCh)
r.PanicsWithError(t, "send on closed channel", func() { ch <- struct{}{} })
} }
func drainProgressUpdateChannel(progress *Progress) { func drainProgressUpdateChannel(progress *Progress) {

View File

@ -82,8 +82,6 @@ func (p *EMLProvider) getFilePathsPerFolder(rules transferRules) (map[string][]s
} }
func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *Progress, ch chan<- Message) { func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *Progress, ch chan<- Message) {
count := uint(len(filePaths))
for _, filePath := range filePaths { for _, filePath := range filePaths {
if progress.shouldStop() { if progress.shouldStop() {
break break
@ -91,6 +89,8 @@ func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *P
msg, err := p.exportMessage(rule, filePath) msg, err := p.exportMessage(rule, filePath)
progress.addMessage(filePath, msg.sourceNames(), msg.targetNames())
// Read and check time in body only if the rule specifies it // Read and check time in body only if the rule specifies it
// to not waste energy. // to not waste energy.
if err == nil && rule.HasTimeLimit() { if err == nil && rule.HasTimeLimit() {
@ -99,17 +99,11 @@ func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *P
err = msgTimeErr err = msgTimeErr
} else if !rule.isTimeInRange(msgTime) { } else if !rule.isTimeInRange(msgTime) {
log.WithField("msg", filePath).Debug("Message skipped due to time") log.WithField("msg", filePath).Debug("Message skipped due to time")
progress.messageSkipped(filePath)
count--
progress.updateCount(rule.SourceMailbox.Name, count)
continue continue
} }
} }
// addMessage is called after time check to not report message
// which should not be exported but any error from reading body
// or parsing time is reported as an error.
progress.addMessage(filePath, rule)
progress.messageExported(filePath, msg.Body, err) progress.messageExported(filePath, msg.Body, err)
if err == nil { if err == nil {
ch <- msg ch <- msg
@ -134,7 +128,7 @@ func (p *EMLProvider) exportMessage(rule *Rule, filePath string) (Message, error
ID: filePath, ID: filePath,
Unread: false, Unread: false,
Body: body, Body: body,
Source: rule.SourceMailbox, Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes, Targets: rule.TargetMailboxes,
}, nil }, nil
} }

View File

@ -61,7 +61,7 @@ func (p *EMLProvider) TransferFrom(rules transferRules, progress *Progress, ch <
func (p *EMLProvider) createFolders(rules transferRules) error { func (p *EMLProvider) createFolders(rules transferRules) error {
for rule := range rules.iterateActiveRules() { for rule := range rules.iterateActiveRules() {
for _, mailbox := range rule.TargetMailboxes { for _, mailbox := range rule.TargetMailboxes {
path := filepath.Join(p.root, mailbox.Name) path := filepath.Join(p.root, sanitizeFileName(mailbox.Name))
if err := os.MkdirAll(path, os.ModePerm); err != nil { if err := os.MkdirAll(path, os.ModePerm); err != nil {
return err return err
} }
@ -71,7 +71,7 @@ func (p *EMLProvider) createFolders(rules transferRules) error {
} }
func (p *EMLProvider) writeFile(msg Message) error { func (p *EMLProvider) writeFile(msg Message) error {
fileName := filepath.Base(msg.ID) fileName := sanitizeFileName(filepath.Base(msg.ID))
if filepath.Ext(fileName) != ".eml" { if filepath.Ext(fileName) != ".eml" {
fileName += ".eml" fileName += ".eml"
} }

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, rule)
} }
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
} }
@ -231,7 +232,7 @@ func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Me
ID: id, ID: id,
Unread: unread, Unread: unread,
Body: body, Body: body,
Source: rule.SourceMailbox, Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes, Targets: rule.TargetMailboxes,
} }
} }

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

@ -18,8 +18,11 @@
package transfer package transfer
import ( import (
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/pkg/errors"
) )
// MBOXProvider implements import and export to/from MBOX structure. // MBOXProvider implements import and export to/from MBOX structure.
@ -44,16 +47,35 @@ func (p *MBOXProvider) ID() string {
// In case the same folder name is used more than once (for example root/a/foo // In case the same folder name is used more than once (for example root/a/foo
// and root/b/foo), it's treated as the same folder. // and root/b/foo), it's treated as the same folder.
func (p *MBOXProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) { func (p *MBOXProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) {
filePaths, err := getFilePathsWithSuffix(p.root, "mbox") filePaths, err := getAllPathsWithSuffix(p.root, ".mbox")
if err != nil { if err != nil {
return nil, err return nil, err
} }
mailboxes := []Mailbox{} mailboxNames := map[string]bool{}
for _, filePath := range filePaths { for _, filePath := range filePaths {
fileName := filepath.Base(filePath) fileName := filepath.Base(filePath)
mailboxName := strings.TrimSuffix(fileName, ".mbox") filePath, err := p.handleAppleMailMBOXStructure(filePath)
if err != nil {
log.WithError(err).Warn("Failed to handle MBOX structure")
continue
}
mailboxName := strings.TrimSuffix(fileName, ".mbox")
mailboxNames[mailboxName] = true
labels, err := getGmailLabelsFromMboxFile(filepath.Join(p.root, filePath))
if err != nil {
log.WithError(err).Error("Failed to get gmail labels from mbox file")
continue
}
for label := range labels {
mailboxNames[label] = true
}
}
mailboxes := []Mailbox{}
for mailboxName := range mailboxNames {
mailboxes = append(mailboxes, Mailbox{ mailboxes = append(mailboxes, Mailbox{
ID: "", ID: "",
Name: mailboxName, Name: mailboxName,
@ -61,6 +83,20 @@ func (p *MBOXProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox,
IsExclusive: false, IsExclusive: false,
}) })
} }
return mailboxes, nil return mailboxes, nil
} }
// handleAppleMailMBOXStructure changes the path of mailbox directory to
// the path of mbox file. Apple Mail MBOX exports has this structure:
// `Folder.mbox` directory with `mbox` file inside.
// Example: `Folder.mbox/mbox` (and this function converts `Folder.mbox`
// to `Folder.mbox/mbox`).
func (p *MBOXProvider) handleAppleMailMBOXStructure(filePath string) (string, error) {
if info, err := os.Stat(filepath.Join(p.root, filePath)); err == nil && info.IsDir() {
if _, err := os.Stat(filepath.Join(p.root, filePath, "mbox")); err != nil {
return "", errors.Wrap(err, "wrong mbox structure")
}
return filepath.Join(filePath, "mbox"), nil
}
return filePath, nil
}

View File

@ -0,0 +1,118 @@
// 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 (
"bufio"
"bytes"
"io"
"mime"
"os"
"strings"
)
type stringSet map[string]bool
const xGmailLabelsHeader = "X-Gmail-Labels"
// filteredOutGmailLabels is set of labels which we don't want to show to users
// as they might be auto-generated by Gmail and unwanted.
var filteredOutGmailLabels = []string{ //nolint[gochecknoglobals]
"Unread",
"Opened",
"IMAP_Junk",
"IMAP_NonJunk",
"IMAP_NotJunk",
"IMAP_$NotJunk",
}
func getGmailLabelsFromMboxFile(filePath string) (stringSet, error) {
f, err := os.Open(filePath) //nolint[gosec]
if err != nil {
return nil, err
}
return getGmailLabelsFromMboxReader(f)
}
func getGmailLabelsFromMboxReader(f io.Reader) (stringSet, error) {
allLabels := stringSet{}
// Scanner is not used as it does not support long lines and some mbox
// files contain very long lines even though that should not be happening.
r := bufio.NewReader(f)
for {
b, isPrefix, err := r.ReadLine()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
for isPrefix {
_, isPrefix, err = r.ReadLine()
if err != nil {
break
}
}
if bytes.HasPrefix(b, []byte(xGmailLabelsHeader)) {
for label := range getGmailLabelsFromValue(string(b)) {
allLabels[label] = true
}
}
}
return allLabels, nil
}
func getGmailLabelsFromMessage(body []byte) (stringSet, error) {
header, err := getMessageHeader(body)
if err != nil {
return nil, err
}
labels := header.Get(xGmailLabelsHeader)
return getGmailLabelsFromValue(labels), nil
}
func getGmailLabelsFromValue(value string) stringSet {
value = strings.TrimPrefix(value, xGmailLabelsHeader+":")
if decoded, err := new(mime.WordDecoder).DecodeHeader(value); err != nil {
log.WithError(err).Error("Failed to decode header")
} else {
value = decoded
}
labels := stringSet{}
for _, label := range strings.Split(value, ",") {
label = strings.TrimSpace(label)
if label == "" {
continue
}
skip := false
for _, filteredOutLabel := range filteredOutGmailLabels {
if label == filteredOutLabel {
skip = true
break
}
}
if skip {
continue
}
labels[label] = true
}
return labels
}

View File

@ -0,0 +1,135 @@
// 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"
"strings"
"testing"
r "github.com/stretchr/testify/require"
)
func TestGetGmailLabelsFromMboxReader(t *testing.T) {
mboxFile := `From - Mon May 4 16:40:31 2020
Subject: Test 1
X-Gmail-Labels: Foo,Bar
hello
From - Mon May 4 16:40:31 2020
Subject: Test 2
X-Gmail-Labels: Foo , Baz
hello
From - Mon May 4 16:40:31 2020
Subject: Test 3
X-Gmail-Labels: ,
hello
From - Mon May 4 16:40:31 2020
Subject: Test 4
X-Gmail-Labels:
hello
From - Mon May 4 16:40:31 2020
Subject: Test 5
hello
`
mboxReader := strings.NewReader(mboxFile)
labels, err := getGmailLabelsFromMboxReader(mboxReader)
r.NoError(t, err)
r.Equal(t, toSet("Foo", "Bar", "Baz"), labels)
}
func TestGetGmailLabelsFromMessage(t *testing.T) {
tests := []struct {
body string
wantLabels stringSet
}{
{`Subject: One
X-Gmail-Labels: Foo,Bar
Hello
`, toSet("Foo", "Bar")},
{`Subject: Two
X-Gmail-Labels: Foo , Bar ,
Hello
`, toSet("Foo", "Bar")},
{`Subject: Three
X-Gmail-Labels: ,
Hello
`, toSet()},
{`Subject: Four
X-Gmail-Labels:
Hello
`, toSet()},
{`Subject: Five
Hello
`, toSet()},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%v", tc.body), func(t *testing.T) {
labels, err := getGmailLabelsFromMessage([]byte(tc.body))
r.NoError(t, err)
r.Equal(t, tc.wantLabels, labels)
})
}
}
func TestGetGmailLabelsFromValue(t *testing.T) {
tests := []struct {
value string
wantLabels stringSet
}{
{"Foo,Bar", toSet("Foo", "Bar")},
{" Foo , Bar ", toSet("Foo", "Bar")},
{" Foo , Bar , ", toSet("Foo", "Bar")},
{" Foo Bar ", toSet("Foo Bar")},
{" , ", toSet()},
{" ", toSet()},
{"", toSet()},
{"=?UTF-8?Q?Archived,Category_personal,test_=F0=9F=98=80=F0=9F=99=83?=", toSet("Archived", "Category personal", "test 😀🙃")},
{"IMAP_NotJunk,Foo,Opened,bar,Unread", toSet("Foo", "bar")},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%v", tc.value), func(t *testing.T) {
labels := getGmailLabelsFromValue(tc.value)
r.Equal(t, tc.wantLabels, labels)
})
}
}
func toSet(items ...string) stringSet {
set := map[string]bool{}
for _, item := range items {
set[item] = true
}
return set
}

View File

@ -34,7 +34,7 @@ func (p *MBOXProvider) TransferTo(rules transferRules, progress *Progress, ch ch
log.Info("Started transfer from MBOX to channel") log.Info("Started transfer from MBOX to channel")
defer log.Info("Finished transfer from MBOX to channel") defer log.Info("Finished transfer from MBOX to channel")
filePathsPerFolder, err := p.getFilePathsPerFolder(rules) filePathsPerFolder, err := p.getFilePathsPerFolder()
if err != nil { if err != nil {
progress.fatal(err) progress.fatal(err)
return return
@ -45,32 +45,29 @@ func (p *MBOXProvider) TransferTo(rules transferRules, progress *Progress, ch ch
} }
for folderName, filePaths := range filePathsPerFolder { for folderName, filePaths := range filePathsPerFolder {
// No error guaranteed by getFilePathsPerFolder. log.WithField("folder", folderName).Debug("Estimating folder counts")
rule, _ := rules.getRuleBySourceMailboxName(folderName)
for _, filePath := range filePaths { for _, filePath := range filePaths {
if progress.shouldStop() { if progress.shouldStop() {
break break
} }
p.updateCount(rule, progress, filePath) p.updateCount(progress, filePath)
} }
} }
progress.countsFinal() progress.countsFinal()
for folderName, filePaths := range filePathsPerFolder { for folderName, filePaths := range filePathsPerFolder {
// No error guaranteed by getFilePathsPerFolder. log.WithField("folder", folderName).Debug("Processing folder")
rule, _ := rules.getRuleBySourceMailboxName(folderName)
log.WithField("rule", rule).Debug("Processing rule")
for _, filePath := range filePaths { for _, filePath := range filePaths {
if progress.shouldStop() { if progress.shouldStop() {
break break
} }
p.transferTo(rule, progress, ch, filePath) p.transferTo(rules, progress, ch, folderName, filePath)
} }
} }
} }
func (p *MBOXProvider) getFilePathsPerFolder(rules transferRules) (map[string][]string, error) { func (p *MBOXProvider) getFilePathsPerFolder() (map[string][]string, error) {
filePaths, err := getFilePathsWithSuffix(p.root, ".mbox") filePaths, err := getAllPathsWithSuffix(p.root, ".mbox")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -78,19 +75,19 @@ func (p *MBOXProvider) getFilePathsPerFolder(rules transferRules) (map[string][]
filePathsMap := map[string][]string{} filePathsMap := map[string][]string{}
for _, filePath := range filePaths { for _, filePath := range filePaths {
fileName := filepath.Base(filePath) fileName := filepath.Base(filePath)
folder := strings.TrimSuffix(fileName, ".mbox") filePath, err := p.handleAppleMailMBOXStructure(filePath)
_, err := rules.getRuleBySourceMailboxName(folder) // Skip unsupported MBOX structures. It was already filtered out in configuration step.
if err != nil { if err != nil {
log.WithField("msg", filePath).Trace("Mailbox skipped due to folder name")
continue continue
} }
folder := strings.TrimSuffix(fileName, ".mbox")
filePathsMap[folder] = append(filePathsMap[folder], filePath) filePathsMap[folder] = append(filePathsMap[folder], filePath)
} }
return filePathsMap, nil return filePathsMap, nil
} }
func (p *MBOXProvider) updateCount(rule *Rule, progress *Progress, filePath string) { func (p *MBOXProvider) updateCount(progress *Progress, filePath string) {
mboxReader := p.openMbox(progress, filePath) mboxReader := p.openMbox(progress, filePath)
if mboxReader == nil { if mboxReader == nil {
return return
@ -107,17 +104,16 @@ func (p *MBOXProvider) updateCount(rule *Rule, progress *Progress, filePath stri
} }
count++ count++
} }
progress.updateCount(rule.SourceMailbox.Name, uint(count)) progress.updateCount(filePath, uint(count))
} }
func (p *MBOXProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Message, filePath string) { func (p *MBOXProvider) transferTo(rules transferRules, progress *Progress, ch chan<- Message, folderName, filePath string) {
mboxReader := p.openMbox(progress, filePath) mboxReader := p.openMbox(progress, filePath)
if mboxReader == nil { if mboxReader == nil {
return return
} }
index := 0 index := 0
count := 0
for { for {
if progress.shouldStop() { if progress.shouldStop() {
break break
@ -134,50 +130,118 @@ func (p *MBOXProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mess
break break
} }
msg, err := p.exportMessage(rule, id, msgReader) msg, err := p.exportMessage(rules, folderName, id, msgReader)
// Read and check time in body only if the rule specifies it progress.addMessage(id, msg.sourceNames(), msg.targetNames())
// to not waste energy.
if err == nil && rule.HasTimeLimit() { if err == nil && len(msg.Targets) == 0 {
msgTime, msgTimeErr := getMessageTime(msg.Body) progress.messageSkipped(id)
if msgTimeErr != nil { continue
err = msgTimeErr
} else if !rule.isTimeInRange(msgTime) {
log.WithField("msg", id).Debug("Message skipped due to time")
continue
}
} }
// Counting only messages filtered by time to update count to correct total.
count++
// addMessage is called after time check to not report message
// which should not be exported but any error from reading body
// or parsing time is reported as an error.
progress.addMessage(id, rule)
progress.messageExported(id, msg.Body, err) progress.messageExported(id, msg.Body, err)
if err == nil { if err == nil {
ch <- msg ch <- msg
} }
} }
progress.updateCount(rule.SourceMailbox.Name, uint(count))
} }
func (p *MBOXProvider) exportMessage(rule *Rule, id string, msgReader io.Reader) (Message, error) { func (p *MBOXProvider) exportMessage(rules transferRules, folderName, id string, msgReader io.Reader) (Message, error) {
body, err := ioutil.ReadAll(msgReader) body, err := ioutil.ReadAll(msgReader)
if err != nil { if err != nil {
return Message{}, errors.Wrap(err, "failed to read message") return Message{}, errors.Wrap(err, "failed to read message")
} }
msgRules := p.getMessageRules(rules, folderName, id, body)
sources := p.getMessageSources(msgRules)
targets := p.getMessageTargets(msgRules, id, body)
return Message{ return Message{
ID: id, ID: id,
Unread: false, Unread: false,
Body: body, Body: body,
Source: rule.SourceMailbox, Sources: sources,
Targets: rule.TargetMailboxes, Targets: targets,
}, nil }, nil
} }
func (p *MBOXProvider) getMessageRules(rules transferRules, folderName, id string, body []byte) []*Rule {
msgRules := []*Rule{}
folderRule, err := rules.getRuleBySourceMailboxName(folderName)
if err != nil {
log.WithField("msg", id).WithField("source", folderName).Debug("Message source doesn't have a rule")
} else if folderRule.Active {
msgRules = append(msgRules, folderRule)
}
gmailLabels, err := getGmailLabelsFromMessage(body)
if err != nil {
log.WithError(err).Error("Failed to get gmail labels, ")
} else {
for label := range gmailLabels {
rule, err := rules.getRuleBySourceMailboxName(label)
if err != nil {
log.WithField("msg", id).WithField("source", label).Debug("Message source doesn't have a rule")
continue
}
if rule.Active {
msgRules = append(msgRules, rule)
}
}
}
return msgRules
}
func (p *MBOXProvider) getMessageSources(msgRules []*Rule) []Mailbox {
sources := []Mailbox{}
for _, rule := range msgRules {
sources = append(sources, rule.SourceMailbox)
}
return sources
}
func (p *MBOXProvider) getMessageTargets(msgRules []*Rule, id string, body []byte) []Mailbox {
targets := []Mailbox{}
haveExclusiveMailbox := false
for _, rule := range msgRules {
// Read and check time in body only if the rule specifies it
// to not waste energy.
if rule.HasTimeLimit() {
msgTime, err := getMessageTime(body)
if err != nil {
log.WithError(err).Error("Failed to parse time, time check skipped")
} else if !rule.isTimeInRange(msgTime) {
log.WithField("msg", id).WithField("source", rule.SourceMailbox.Name).Debug("Message skipped due to time")
continue
}
}
for _, newTarget := range rule.TargetMailboxes {
// msgRules is sorted. The first rule is based on the folder name,
// followed by the order from X-Gmail-Labels. The rule based on
// the folder name should have priority for exclusive target.
if newTarget.IsExclusive && haveExclusiveMailbox {
continue
}
found := false
for _, target := range targets {
if target.Hash() == newTarget.Hash() {
found = true
break
}
}
if found {
continue
}
if newTarget.IsExclusive {
haveExclusiveMailbox = true
}
targets = append(targets, newTarget)
}
}
return targets
}
func (p *MBOXProvider) openMbox(progress *Progress, mboxPath string) *mbox.Reader { func (p *MBOXProvider) openMbox(progress *Progress, mboxPath string) *mbox.Reader {
mboxPath = filepath.Join(p.root, mboxPath) mboxPath = filepath.Join(p.root, mboxPath)
mboxFile, err := os.Open(mboxPath) //nolint[gosec] mboxFile, err := os.Open(mboxPath) //nolint[gosec]

View File

@ -57,7 +57,7 @@ func (p *MBOXProvider) TransferFrom(rules transferRules, progress *Progress, ch
func (p *MBOXProvider) writeMessage(msg Message) error { func (p *MBOXProvider) writeMessage(msg Message) error {
var multiErr error var multiErr error
for _, mailbox := range msg.Targets { for _, mailbox := range msg.Targets {
mboxName := filepath.Base(mailbox.Name) mboxName := sanitizeFileName(mailbox.Name)
if !strings.HasSuffix(mboxName, ".mbox") { if !strings.HasSuffix(mboxName, ".mbox") {
mboxName += ".mbox" mboxName += ".mbox"
} }

View File

@ -35,27 +35,35 @@ func newTestMBOXProvider(path string) *MBOXProvider {
} }
func TestMBOXProviderMailboxes(t *testing.T) { func TestMBOXProviderMailboxes(t *testing.T) {
provider := newTestMBOXProvider("")
tests := []struct { tests := []struct {
provider *MBOXProvider
includeEmpty bool includeEmpty bool
wantMailboxes []Mailbox wantMailboxes []Mailbox
}{ }{
{true, []Mailbox{ {newTestMBOXProvider(""), true, []Mailbox{
{Name: "All Mail"},
{Name: "Foo"}, {Name: "Foo"},
{Name: "Bar"},
{Name: "Inbox"}, {Name: "Inbox"},
}}, }},
{false, []Mailbox{ {newTestMBOXProvider(""), false, []Mailbox{
{Name: "All Mail"},
{Name: "Foo"}, {Name: "Foo"},
{Name: "Bar"},
{Name: "Inbox"}, {Name: "Inbox"},
}}, }},
{newTestMBOXProvider("testdata/mbox-applemail"), true, []Mailbox{
{Name: "All Mail"},
{Name: "Foo"},
{Name: "Bar"},
}},
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(fmt.Sprintf("%v", tc.includeEmpty), func(t *testing.T) { t.Run(fmt.Sprintf("%v", tc.includeEmpty), func(t *testing.T) {
mailboxes, err := provider.Mailboxes(tc.includeEmpty, false) mailboxes, err := tc.provider.Mailboxes(tc.includeEmpty, false)
r.NoError(t, err) r.NoError(t, err)
r.Equal(t, tc.wantMailboxes, mailboxes) r.ElementsMatch(t, tc.wantMailboxes, mailboxes)
}) })
} }
} }
@ -67,14 +75,47 @@ func TestMBOXProviderTransferTo(t *testing.T) {
defer rulesClose() defer rulesClose()
setupMBOXRules(rules) setupMBOXRules(rules)
testTransferTo(t, rules, provider, []string{ msgs := testTransferTo(t, rules, provider, []string{
"All Mail.mbox:1",
"All Mail.mbox:2",
"Foo.mbox:1", "Foo.mbox:1",
"Inbox.mbox:1", "Inbox.mbox:1",
}) })
got := map[string][]string{}
for _, msg := range msgs {
got[msg.ID] = msg.targetNames()
}
r.Equal(t, map[string][]string{
"All Mail.mbox:1": {"Archive", "Foo"}, // Bar is not in rules.
"All Mail.mbox:2": {"Archive", "Foo"},
"Foo.mbox:1": {"Foo"},
"Inbox.mbox:1": {"Inbox"},
}, got)
}
func TestMBOXProviderTransferToAppleMail(t *testing.T) {
provider := newTestMBOXProvider("testdata/mbox-applemail")
rules, rulesClose := newTestRules(t)
defer rulesClose()
setupMBOXRules(rules)
msgs := testTransferTo(t, rules, provider, []string{
"All Mail.mbox/mbox:1",
"All Mail.mbox/mbox:2",
})
got := map[string][]string{}
for _, msg := range msgs {
got[msg.ID] = msg.targetNames()
}
r.Equal(t, map[string][]string{
"All Mail.mbox/mbox:1": {"Archive", "Foo"}, // Bar is not in rules.
"All Mail.mbox/mbox:2": {"Archive", "Foo"},
}, got)
} }
func TestMBOXProviderTransferFrom(t *testing.T) { func TestMBOXProviderTransferFrom(t *testing.T) {
dir, err := ioutil.TempDir("", "eml") dir, err := ioutil.TempDir("", "mbox")
r.NoError(t, err) r.NoError(t, err)
defer os.RemoveAll(dir) //nolint[errcheck] defer os.RemoveAll(dir) //nolint[errcheck]
@ -94,7 +135,7 @@ func TestMBOXProviderTransferFrom(t *testing.T) {
} }
func TestMBOXProviderTransferFromTo(t *testing.T) { func TestMBOXProviderTransferFromTo(t *testing.T) {
dir, err := ioutil.TempDir("", "eml") dir, err := ioutil.TempDir("", "mbox")
r.NoError(t, err) r.NoError(t, err)
defer os.RemoveAll(dir) //nolint[errcheck] defer os.RemoveAll(dir) //nolint[errcheck]
@ -103,23 +144,80 @@ func TestMBOXProviderTransferFromTo(t *testing.T) {
rules, rulesClose := newTestRules(t) rules, rulesClose := newTestRules(t)
defer rulesClose() defer rulesClose()
setupEMLRules(rules) setupMBOXRules(rules)
testTransferFromTo(t, rules, source, target, 5*time.Second) testTransferFromTo(t, rules, source, target, 5*time.Second)
checkMBOXFileStructure(t, dir, []string{ checkMBOXFileStructure(t, dir, []string{
"Archive.mbox",
"Foo.mbox", "Foo.mbox",
"Inbox.mbox", "Inbox.mbox",
}) })
} }
func TestMBOXProviderGetMessageRules(t *testing.T) {
provider := newTestMBOXProvider("")
body := []byte(`Subject: Test
X-Gmail-Labels: foo,bar
`)
rules := transferRules{
rules: map[string]*Rule{
"1": {Active: true, SourceMailbox: Mailbox{Name: "folder"}},
"2": {Active: false, SourceMailbox: Mailbox{Name: "foo"}},
"3": {Active: true, SourceMailbox: Mailbox{Name: "bar"}},
"4": {Active: false, SourceMailbox: Mailbox{Name: "baz"}},
"5": {Active: true, SourceMailbox: Mailbox{Name: "other"}},
},
}
gotRules := provider.getMessageRules(rules, "folder", "id", body)
r.Equal(t, 2, len(gotRules))
r.Equal(t, "folder", gotRules[0].SourceMailbox.Name)
r.Equal(t, "bar", gotRules[1].SourceMailbox.Name)
}
func TestMBOXProviderGetMessageTargetsReturnsOnlyOneFolder(t *testing.T) {
provider := newTestMBOXProvider("")
folderA := Mailbox{Name: "Folder A", IsExclusive: true}
folderB := Mailbox{Name: "Folder B", IsExclusive: true}
labelA := Mailbox{Name: "Label A", IsExclusive: false}
labelB := Mailbox{Name: "Label B", IsExclusive: false}
labelC := Mailbox{Name: "Label C", IsExclusive: false}
rule1 := &Rule{TargetMailboxes: []Mailbox{folderA, labelA, labelB}}
rule2 := &Rule{TargetMailboxes: []Mailbox{folderB, labelC}}
rule3 := &Rule{TargetMailboxes: []Mailbox{folderB}}
tests := []struct {
rules []*Rule
wantMailboxes []Mailbox
}{
{[]*Rule{}, []Mailbox{}},
{[]*Rule{rule1}, []Mailbox{folderA, labelA, labelB}},
{[]*Rule{rule1, rule2}, []Mailbox{folderA, labelA, labelB, labelC}},
{[]*Rule{rule1, rule3}, []Mailbox{folderA, labelA, labelB}},
{[]*Rule{rule3, rule1}, []Mailbox{folderB, labelA, labelB}},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%v", tc.rules), func(t *testing.T) {
mailboxes := provider.getMessageTargets(tc.rules, "", []byte(""))
r.Equal(t, tc.wantMailboxes, mailboxes)
})
}
}
func setupMBOXRules(rules transferRules) { func setupMBOXRules(rules transferRules) {
_ = rules.setRule(Mailbox{Name: "All Mail"}, []Mailbox{{Name: "Archive"}}, 0, 0)
_ = rules.setRule(Mailbox{Name: "Inbox"}, []Mailbox{{Name: "Inbox"}}, 0, 0) _ = rules.setRule(Mailbox{Name: "Inbox"}, []Mailbox{{Name: "Inbox"}}, 0, 0)
_ = rules.setRule(Mailbox{Name: "Foo"}, []Mailbox{{Name: "Foo"}}, 0, 0) _ = rules.setRule(Mailbox{Name: "Foo"}, []Mailbox{{Name: "Foo"}}, 0, 0)
} }
func checkMBOXFileStructure(t *testing.T, root string, expectedFiles []string) { func checkMBOXFileStructure(t *testing.T, root string, expectedFiles []string) {
files, err := getFilePathsWithSuffix(root, ".mbox") files, err := getAllPathsWithSuffix(root, ".mbox")
r.NoError(t, err) r.NoError(t, err)
r.Equal(t, expectedFiles, files) r.Equal(t, expectedFiles, files)
} }

View File

@ -123,7 +123,7 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
} }
msgID := fmt.Sprintf("%s_%s", rule.SourceMailbox.ID, pmapiMessage.ID) msgID := fmt.Sprintf("%s_%s", rule.SourceMailbox.ID, pmapiMessage.ID)
progress.addMessage(msgID, rule) progress.addMessage(msgID, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
msg, err := p.exportMessage(rule, progress, pmapiMessage.ID, msgID, skipEncryptedMessages) msg, err := p.exportMessage(rule, progress, pmapiMessage.ID, msgID, skipEncryptedMessages)
progress.messageExported(msgID, msg.Body, err) progress.messageExported(msgID, msg.Body, err)
if err == nil { if err == nil {
@ -177,7 +177,7 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
ID: msgID, ID: msgID,
Unread: unread, Unread: unread,
Body: body, Body: body,
Source: rule.SourceMailbox, Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes, Targets: rule.TargetMailboxes,
}, nil }, nil
} }

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,11 +177,14 @@ 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() {
return
}
importMsgReqSize := len(importMsgReq.Body) importMsgReqSize := len(importMsgReq.Body)
if p.nextImportRequestsSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.nextImportRequests) == pmapiImportBatchMaxItems { if p.nextImportRequestsSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.nextImportRequests) == pmapiImportBatchMaxItems {
@ -187,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
@ -212,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

@ -72,7 +72,8 @@ func (p *PMAPIProvider) tryReconnect() error {
func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*pmapi.Message, count int, err error) { func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*pmapi.Message, count int, err error) {
err = p.ensureConnection(func() error { err = p.ensureConnection(func() error {
key := fmt.Sprintf("%s_%d", filter.LabelID, filter.Page) // Sort is used in the key so the filter is different for estimating and real fetching.
key := fmt.Sprintf("%s_%s_%d", filter.LabelID, filter.Sort, filter.Page)
p.timeIt.start("listing", key) p.timeIt.start("listing", key)
defer p.timeIt.stop("listing", key) defer p.timeIt.stop("listing", key)
@ -117,8 +118,10 @@ func (p *PMAPIProvider) createDraft(msgSourceID string, message *pmapi.Message,
func (p *PMAPIProvider) createAttachment(msgSourceID string, att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error) { func (p *PMAPIProvider) createAttachment(msgSourceID string, att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error) {
err = p.ensureConnection(func() error { err = p.ensureConnection(func() error {
p.timeIt.start("upload", msgSourceID) // Use some attributes from attachment to have unique key for each call.
defer p.timeIt.stop("upload", msgSourceID) key := fmt.Sprintf("%s_%s_%d", msgSourceID, att.Name, att.Size)
p.timeIt.start("upload", key)
defer p.timeIt.stop("upload", key)
created, err = p.client().CreateAttachment(att, r, sig) created, err = p.client().CreateAttachment(att, r, sig)
return err return err

View File

@ -43,7 +43,7 @@ hello
`, subject)) `, subject))
} }
func testTransferTo(t *testing.T, rules transferRules, provider SourceProvider, expectedMessageIDs []string) { func testTransferTo(t *testing.T, rules transferRules, provider SourceProvider, expectedMessageIDs []string) []Message {
progress := newProgress(log, nil) progress := newProgress(log, nil)
drainProgressUpdateChannel(&progress) drainProgressUpdateChannel(&progress)
@ -53,13 +53,17 @@ func testTransferTo(t *testing.T, rules transferRules, provider SourceProvider,
close(ch) close(ch)
}() }()
msgs := []Message{}
gotMessageIDs := []string{} gotMessageIDs := []string{}
for msg := range ch { for msg := range ch {
msgs = append(msgs, msg)
gotMessageIDs = append(gotMessageIDs, msg.ID) gotMessageIDs = append(gotMessageIDs, msg.ID)
} }
r.ElementsMatch(t, expectedMessageIDs, gotMessageIDs) r.ElementsMatch(t, expectedMessageIDs, gotMessageIDs)
r.Empty(t, progress.GetFailedMessages()) r.Empty(t, progress.GetFailedMessages())
return msgs
} }
func testTransferFrom(t *testing.T, rules transferRules, provider TargetProvider, messages []Message) { func testTransferFrom(t *testing.T, rules transferRules, provider TargetProvider, messages []Message) {
@ -69,7 +73,7 @@ func testTransferFrom(t *testing.T, rules transferRules, provider TargetProvider
ch := make(chan Message) ch := make(chan Message)
go func() { go func() {
for _, message := range messages { for _, message := range messages {
progress.addMessage(message.ID, nil) progress.addMessage(message.ID, []string{}, []string{})
progress.messageExported(message.ID, []byte(""), nil) progress.messageExported(message.ID, []byte(""), nil)
ch <- message ch <- message
} }
@ -81,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

@ -114,7 +114,7 @@ type messageReport struct {
SourceID string SourceID string
TargetID string TargetID string
BodyHash string BodyHash string
SourceMailbox string SourceMailboxes []string
TargetMailboxes []string TargetMailboxes []string
Error string Error string
@ -130,8 +130,8 @@ func newMessageReportFromMessageStatus(messageStatus *MessageStatus, includePriv
SourceID: messageStatus.SourceID, SourceID: messageStatus.SourceID,
TargetID: messageStatus.targetID, TargetID: messageStatus.targetID,
BodyHash: messageStatus.bodyHash, BodyHash: messageStatus.bodyHash,
SourceMailbox: messageStatus.rule.SourceMailbox.Name, SourceMailboxes: messageStatus.sourceNames,
TargetMailboxes: messageStatus.rule.TargetMailboxNames(), TargetMailboxes: messageStatus.targetNames,
Error: messageStatus.GetErrorMessage(), Error: messageStatus.GetErrorMessage(),
} }

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

@ -0,0 +1,16 @@
From - Mon May 4 16:40:31 2020
From: Bridge Test <bridgetest@pm.test>
To: Bridge Test <bridgetest@protonmail.com>
Subject: Test 1
X-Gmail-Labels: Foo,Bar
hello
From - Mon May 4 16:40:31 2020
From: Bridge Test <bridgetest@pm.test>
To: Bridge Test <bridgetest@protonmail.com>
Subject: Test 2
X-Gmail-Labels: Foo
hello

View File

@ -0,0 +1,16 @@
From - Mon May 4 16:40:31 2020
From: Bridge Test <bridgetest@pm.test>
To: Bridge Test <bridgetest@protonmail.com>
Subject: Test 1
X-Gmail-Labels: Foo,Bar
hello
From - Mon May 4 16:40:31 2020
From: Bridge Test <bridgetest@pm.test>
To: Bridge Test <bridgetest@protonmail.com>
Subject: Test 2
X-Gmail-Labels: Foo
hello

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

@ -24,9 +24,11 @@ import (
"net/mail" "net/mail"
"net/textproto" "net/textproto"
"path/filepath" "path/filepath"
"runtime"
"sort" "sort"
"strings" "strings"
"github.com/ProtonMail/go-rfc5322"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -81,7 +83,7 @@ func getFolderNamesWithFileSuffix(root, fileSuffix string) ([]string, error) {
// getFilePathsWithSuffix collects all file names with `suffix` under `root`. // getFilePathsWithSuffix collects all file names with `suffix` under `root`.
// File names will be with relative path based to `root`. // File names will be with relative path based to `root`.
func getFilePathsWithSuffix(root, suffix string) ([]string, error) { func getFilePathsWithSuffix(root, suffix string) ([]string, error) {
fileNames, err := getFilePathsWithSuffixInner("", root, suffix) fileNames, err := getFilePathsWithSuffixInner("", root, suffix, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -89,7 +91,18 @@ func getFilePathsWithSuffix(root, suffix string) ([]string, error) {
return fileNames, err return fileNames, err
} }
func getFilePathsWithSuffixInner(prefix, root, suffix string) ([]string, error) { // getAllPathsWithSuffix is the same as getFilePathsWithSuffix but includes
// also directories.
func getAllPathsWithSuffix(root, suffix string) ([]string, error) {
fileNames, err := getFilePathsWithSuffixInner("", root, suffix, true)
if err != nil {
return nil, err
}
sort.Strings(fileNames)
return fileNames, err
}
func getFilePathsWithSuffixInner(prefix, root, suffix string, includeDir bool) ([]string, error) {
fileNames := []string{} fileNames := []string{}
files, err := ioutil.ReadDir(root) files, err := ioutil.ReadDir(root)
@ -103,10 +116,14 @@ func getFilePathsWithSuffixInner(prefix, root, suffix string) ([]string, error)
fileNames = append(fileNames, filepath.Join(prefix, file.Name())) fileNames = append(fileNames, filepath.Join(prefix, file.Name()))
} }
} else { } else {
if includeDir && strings.HasSuffix(file.Name(), suffix) {
fileNames = append(fileNames, filepath.Join(prefix, file.Name()))
}
subfolderFileNames, err := getFilePathsWithSuffixInner( subfolderFileNames, err := getFilePathsWithSuffixInner(
filepath.Join(prefix, file.Name()), filepath.Join(prefix, file.Name()),
filepath.Join(root, file.Name()), filepath.Join(root, file.Name()),
suffix, suffix,
includeDir,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -120,14 +137,21 @@ func getFilePathsWithSuffixInner(prefix, root, suffix string) ([]string, error)
// getMessageTime returns time of the message specified in the message header. // getMessageTime returns time of the message specified in the message header.
func getMessageTime(body []byte) (int64, error) { func getMessageTime(body []byte) (int64, error) {
mailHeader, err := getMessageHeader(body) hdr, err := getMessageHeader(body)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if t, err := mailHeader.Date(); err == nil && !t.IsZero() {
return t.Unix(), nil t, err := rfc5322.ParseDateTime(hdr.Get("Date"))
if err != nil {
return 0, err
} }
return 0, nil
if t.IsZero() {
return 0, nil
}
return t.Unix(), nil
} }
// getMessageHeader returns headers of the message body. // getMessageHeader returns headers of the message body.
@ -139,3 +163,24 @@ func getMessageHeader(body []byte) (mail.Header, error) {
} }
return mail.Header(header), nil return mail.Header(header), nil
} }
// sanitizeFileName replaces problematic special characters with underscore.
func sanitizeFileName(fileName string) string {
if len(fileName) == 0 {
return fileName
}
if runtime.GOOS != "windows" && (fileName[0] == '-' || fileName[0] == '.') { //nolint[goconst]
fileName = "_" + fileName[1:]
}
return strings.Map(func(r rune) rune {
switch r {
case '\\', '/', ':', '*', '?', '"', '<', '>', '|':
return '_'
case '[', ']', '(', ')', '{', '}', '^', '#', '%', '&', '!', '@', '+', '=', '\'', '~':
if runtime.GOOS != "windows" {
return '_'
}
}
return r
}, fileName)
}

View File

@ -21,6 +21,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
r "github.com/stretchr/testify/require" r "github.com/stretchr/testify/require"
@ -38,6 +39,7 @@ func TestGetFolderNames(t *testing.T) {
"", "",
[]string{ []string{
"bar", "bar",
"bar.mbox",
"baz", "baz",
filepath.Base(root), filepath.Base(root),
"foo", "foo",
@ -94,6 +96,13 @@ func TestGetFilePathsWithSuffix(t *testing.T) {
"test/foo/msg9.eml", "test/foo/msg9.eml",
}, },
}, },
{
".mbox",
[]string{
"bar.mbox",
"foo.mbox",
},
},
{ {
".txt", ".txt",
[]string{ []string{
@ -108,7 +117,7 @@ func TestGetFilePathsWithSuffix(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.suffix, func(t *testing.T) { t.Run(tc.suffix, func(t *testing.T) {
paths, err := getFilePathsWithSuffix(root, tc.suffix) paths, err := getAllPathsWithSuffix(root, tc.suffix)
r.NoError(t, err) r.NoError(t, err)
r.Equal(t, tc.wantPaths, paths) r.Equal(t, tc.wantPaths, paths)
}) })
@ -124,6 +133,7 @@ func createTestingFolderStructure(t *testing.T) (string, func()) {
"foo/baz", "foo/baz",
"test/foo", "test/foo",
"qwerty", "qwerty",
"bar.mbox",
} { } {
err = os.MkdirAll(filepath.Join(root, path), os.ModePerm) err = os.MkdirAll(filepath.Join(root, path), os.ModePerm)
r.NoError(t, err) r.NoError(t, err)
@ -141,6 +151,8 @@ func createTestingFolderStructure(t *testing.T) (string, func()) {
"test/foo/msg9.eml", "test/foo/msg9.eml",
"msg10.eml", "msg10.eml",
"info.txt", "info.txt",
"foo.mbox",
"bar.mbox/mbox", // Apple Mail mbox export format.
} { } {
f, err := os.Create(filepath.Join(root, path)) f, err := os.Create(filepath.Join(root, path))
r.NoError(t, err) r.NoError(t, err)
@ -188,3 +200,26 @@ Body
r.Equal(t, header.Get("subject"), "Hello") r.Equal(t, header.Get("subject"), "Hello")
r.Equal(t, header.Get("from"), "user@example.com") r.Equal(t, header.Get("from"), "user@example.com")
} }
func TestSanitizeFileName(t *testing.T) {
tests := map[string]string{
"hello": "hello",
"a\\b/c:*?d\"<>|e": "a_b_c___d____e",
}
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
tests[".hello"] = "_hello"
tests["-hello"] = "_hello"
}
if runtime.GOOS == "windows" {
tests["[hello]&@=~~"] = "_hello______"
}
for path, wantPath := range tests {
path := path
wantPath := wantPath
t.Run(path, func(t *testing.T) {
gotPath := sanitizeFileName(path)
r.Equal(t, wantPath, gotPath)
})
}
}

View File

@ -45,7 +45,7 @@ func syncFolders(localPath, updatePath string) (err error) {
} }
func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) { func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
log.Debug("remove missing") log.WithField("from", folderToCleanPath).Debug("Remove missing.")
// Create list of files. // Create list of files.
existingRelPaths := map[string]bool{} existingRelPaths := map[string]bool{}
err = filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error { err = filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error {
@ -56,7 +56,7 @@ func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
if walkErr != nil { if walkErr != nil {
return walkErr return walkErr
} }
log.Debug("path to keep ", relPath) log.WithField("path", relPath).Trace("Keep the path.")
existingRelPaths[relPath] = true existingRelPaths[relPath] = true
return nil return nil
}) })
@ -95,12 +95,18 @@ func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
} }
func restoreFromBackup(backupDir, localPath string) { func restoreFromBackup(backupDir, localPath string) {
log.Error("recovering from ", backupDir, " to ", localPath) log.WithField("from", backupDir).
_ = copyRecursively(backupDir, localPath) WithField("to", localPath).
Error("recovering")
if err := copyRecursively(backupDir, localPath); err != nil {
log.WithField("from", backupDir).
WithField("to", localPath).
Error("Not able to recover.")
}
} }
func createBackup(srcFile, dstDir string) (err error) { func createBackup(srcFile, dstDir string) (err error) {
log.Debug("backup ", srcFile, " in ", dstDir) log.WithField("from", srcFile).WithField("to", dstDir).Debug("Create backup")
if err = mkdirAllClear(dstDir); err != nil { if err = mkdirAllClear(dstDir); err != nil {
return return
} }

View File

@ -107,7 +107,7 @@ func NewImportExport(updateTempDir string) *Updates {
versionFileBaseName: "current_version_ie", versionFileBaseName: "current_version_ie",
updateFileBaseName: "ie/ie_upgrade", updateFileBaseName: "ie/ie_upgrade",
linuxFileBaseName: "ie/protonmail-import-export-app", linuxFileBaseName: "ie/protonmail-import-export-app",
macAppBundleName: "Import-Export app.app", macAppBundleName: "ProtonMail Import-Export app.app",
} }
} }
@ -310,7 +310,9 @@ func (u *Updates) StartUpgrade(currentStatus chan<- Progress) { // nolint[funlen
status.UpdateDescription(InfoUpgrading) status.UpdateDescription(InfoUpgrading)
switch runtime.GOOS { switch runtime.GOOS {
case "windows": //nolint[goconst] case "windows": //nolint[goconst]
installerFile := strings.Split(u.winInstallerFile, "/")[1] // Cannot use filepath.Base on windows it has different delimiter
split := strings.Split(u.winInstallerFile, "/")
installerFile := split[len(split)-1]
cmd := exec.Command("./" + installerFile) // nolint[gosec] cmd := exec.Command("./" + installerFile) // nolint[gosec]
cmd.Dir = u.updateTempDir cmd.Dir = u.updateTempDir
status.Err = cmd.Start() status.Err = cmd.Start()
@ -326,10 +328,15 @@ func (u *Updates) StartUpgrade(currentStatus chan<- Progress) { // nolint[funlen
localPath = filepath.Dir(localPath) // .app localPath = filepath.Dir(localPath) // .app
updatePath := filepath.Join(u.updateTempDir, u.macAppBundleName) updatePath := filepath.Join(u.updateTempDir, u.macAppBundleName)
log.Warn("localPath ", localPath) log.WithField("local", localPath).
log.Warn("updatePath ", updatePath) WithField("update", updatePath).
Info("Syncing folders..")
status.Err = syncFolders(localPath, updatePath) status.Err = syncFolders(localPath, updatePath)
if status.Err != nil { if status.Err != nil {
log.WithField("from", localPath).
WithField("to", updatePath).
WithError(status.Err).
Error("Sync failed.")
return return
} }
status.UpdateDescription(InfoRestartApp) status.UpdateDescription(InfoRestartApp)

View File

@ -5,11 +5,12 @@
package mocks package mocks
import ( import (
reflect "reflect"
store "github.com/ProtonMail/proton-bridge/internal/store" store "github.com/ProtonMail/proton-bridge/internal/store"
credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials" credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials"
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"
) )
// MockConfiger is a mock of Configer interface // MockConfiger is a mock of Configer interface

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