forked from Silverfish/proton-bridge
Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d576beeb8 | |||
| e3332d1cb6 | |||
| f59f68f894 | |||
| 9c881a02d6 | |||
| 7b21c2d734 | |||
| 9fdc5960bf | |||
| fe853efe32 | |||
| 9b82c03959 | |||
| 914d1b27b5 | |||
| f295d03641 | |||
| 8515f6e6ac | |||
| 4d330e24c1 | |||
| a7a52bc57e | |||
| 3cef7985d3 | |||
| 40db822450 | |||
| 2de202ca02 | |||
| 38eb9fdac7 | |||
| f469d34781 | |||
| 33dfc5ce09 | |||
| 2100e2ff7c | |||
| e9b7cce138 | |||
| 6877a5a15d | |||
| 64206e69bd | |||
| 7643c76cb1 | |||
| b0f59273d3 | |||
| af8eb9d37d | |||
| 635e51f32f | |||
| ca962ce5ad | |||
| a50266cdc0 | |||
| 6230200218 | |||
| f96cd167ef | |||
| 072ce54fe1 | |||
| d043cb9086 | |||
| 1f31df3a94 | |||
| 9ee30e4923 | |||
| 7b44f12ab1 | |||
| 874882b554 | |||
| 945bdf4c60 | |||
| 6e1e5a2afe | |||
| b709b51790 | |||
| d380485bb6 | |||
| 87c8228cd0 | |||
| 152046bf97 | |||
| a0fbed5859 | |||
| 89e9e17d26 | |||
| b595247392 | |||
| 9d50a8cef2 | |||
| f888176485 | |||
| 2f9876ad74 | |||
| 53404122cc | |||
| ba65494fce | |||
| 70645c1732 | |||
| 1055e60d27 | |||
| e04196f8a0 | |||
| 11a0dec047 | |||
| b9740e1b7d | |||
| f0695eb870 | |||
| a40018cdf9 | |||
| 5b7eabe21a | |||
| d5d60aa11b | |||
| a62fa132e6 | |||
| 052395f917 | |||
| 9a77650004 | |||
| f1d70361c9 | |||
| 3496599723 | |||
| 9e0635a6a4 | |||
| 10509621ce | |||
| 3727ecdfe5 | |||
| ac71d22e86 | |||
| bc81356d53 | |||
| 881cb64beb | |||
| 1286e57b63 | |||
| fe5f73d96e | |||
| 8f7a8b31a3 | |||
| 68db35d5d4 | |||
| df17017ced | |||
| 5c48332b0e | |||
| 8985738af5 | |||
| 2d8a676dd5 | |||
| 7e0a9f398c | |||
| 9af5769510 | |||
| bb46d9a009 | |||
| 606b42a6e7 | |||
| d547f5ea22 | |||
| 563b4889e3 | |||
| b449beb68c | |||
| f9d58f4f9c | |||
| 1dfec9902e | |||
| 79cafee2eb | |||
| 64fbcdc1ca | |||
| e4a341af3a | |||
| e0292fe957 | |||
| ef85c8df24 | |||
| 719d369c2a | |||
| 51b6f95342 | |||
| 26fb1fc34d | |||
| ae1578a5e2 | |||
| cfd8e56277 | |||
| 4893931a8d | |||
| 932928ddc8 | |||
| a33e414f01 | |||
| 43d54c8f4f | |||
| 6cbc11a75d | |||
| a21bb130e1 | |||
| 12403785af | |||
| b4892855d4 | |||
| 7ff67f2217 | |||
| 4912c27be8 | |||
| 288ba11452 | |||
| 7874183052 | |||
| b12873f1df | |||
| dc9851f8ea | |||
| ec73170e9b | |||
| 68616e470c | |||
| 53cd2ff524 | |||
| 51c8bceed8 | |||
| e02c7c7f06 | |||
| 15c1d7bc24 | |||
| a89a3f6612 | |||
| d956b04062 | |||
| ef1671d4ab | |||
| fe926cbd57 | |||
| e01747e3b9 | |||
| 85220848d0 | |||
| 70f91ae55b | |||
| a73b30ed9e | |||
| 7337f78d4a | |||
| 9b5da91f7c | |||
| c7669b950f | |||
| b3ed8d51a7 | |||
| 60b7d980f4 | |||
| abf2238e6f | |||
| b4a358c084 | |||
| 3606a0ab9f | |||
| c5665d0dd7 | |||
| d6464c0048 | |||
| 5496a26f73 | |||
| ec9a799fe9 | |||
| 730abadfc3 | |||
| 60e1548685 | |||
| 7430c7f1f5 | |||
| 6671b78799 | |||
| c7578cf53c | |||
| 66e04dd5ed | |||
| 803353e300 | |||
| f3773c9d78 | |||
| 41ac61bbe8 | |||
| 0d3d6747ac | |||
| eaa9a458c4 | |||
| 46e5cb9c83 | |||
| dc5387a512 | |||
| 4b7c234e78 | |||
| 5bca6fc3cf | |||
| 97b64ebb70 | |||
| 9b3cc9dc34 | |||
| afeed4a801 | |||
| dd70b30f76 | |||
| 3e8e3c912b | |||
| 5d0e3f36b4 | |||
| da751a38e3 | |||
| f9af17dd9b | |||
| f622ecf678 | |||
| 475e673b87 | |||
| 3916ddc8e4 | |||
| ef2ace0afe | |||
| b5d3737a7e | |||
| d872d77cf5 | |||
| 1f17628399 | |||
| 4ab8f7d6b5 | |||
| fa5f4acdac | |||
| 642666fa59 | |||
| a2cf5374b9 | |||
| 6a7a77fc51 | |||
| f4dfadce52 | |||
| 9ba08e5edb | |||
| 9821b5bbc2 | |||
| 5343a6fc0f | |||
| 180c6699e0 | |||
| 7d1b0d0a40 | |||
| caff73d06c | |||
| f4d073b4cf | |||
| 65d8b382d0 | |||
| 0e7e13211b | |||
| 7e1af9ff4e | |||
| 37186846db | |||
| a5a61c9428 | |||
| ea01c155da | |||
| f4374a02da | |||
| 0d4d95360f | |||
| f88071b2ca | |||
| e01a523ae3 | |||
| c6b18b45b5 | |||
| a7da66ccbc | |||
| 8bd74c5edc | |||
| 2b36d3ab7b | |||
| 45b863f931 | |||
| 953150cfdb | |||
| 6ea3fc1963 | |||
| 7207a5d59e | |||
| dd2264da6f | |||
| 9261b6337e | |||
| 4f6e8c30c7 | |||
| 614a00eac1 | |||
| de58c7a905 | |||
| 2e439e17cf | |||
| f73aeec97f | |||
| 8a7b4bb919 | |||
| 78fd73ee2a | |||
| 33bf64cc4e | |||
| bb1d27a5be | |||
| 9218598140 | |||
| af89931f05 | |||
| 84147a2cb0 | |||
| 2269a9edb7 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1 +1 @@
|
||||
Changelog.md merge=union
|
||||
unreleased.md merge=union
|
||||
|
||||
@ -27,6 +27,9 @@ Issue tracker is ONLY used for reporting bugs with technical details. "It doesn'
|
||||
3.
|
||||
4.
|
||||
|
||||
## Version Information
|
||||
<!--- Which version of the app(s) were you using when you experienced this issue? -->
|
||||
|
||||
## Context (Environment)
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -29,6 +29,7 @@ frontend/qml/*.qmlc
|
||||
# Build files
|
||||
bridge_darwin_*.tgz
|
||||
cmd/Desktop-Bridge/deploy
|
||||
cmd/Import-Export/deploy
|
||||
internal/frontend/qt*/moc.cpp
|
||||
internal/frontend/qt*/moc.go
|
||||
internal/frontend/qt*/moc.h
|
||||
@ -43,4 +44,4 @@ internal/frontend/rcc.qrc
|
||||
internal/frontend/rcc_cgo_*.go
|
||||
vendor-cache/
|
||||
|
||||
/main.go
|
||||
/main.go
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:latest
|
||||
|
||||
before_script:
|
||||
- eval $(ssh-agent -s)
|
||||
@ -45,6 +45,8 @@ lint:
|
||||
- branches
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test:
|
||||
stage: test
|
||||
@ -60,6 +62,8 @@ test:
|
||||
- pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'`
|
||||
# Then finally run the tests
|
||||
- make test
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-integration:
|
||||
stage: test
|
||||
@ -67,6 +71,8 @@ test-integration:
|
||||
- branches
|
||||
script:
|
||||
- VERBOSITY=debug make -C test test
|
||||
tags:
|
||||
- large
|
||||
|
||||
dependency-updates:
|
||||
stage: test
|
||||
@ -82,7 +88,11 @@ dependency-updates:
|
||||
script:
|
||||
- make build
|
||||
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:
|
||||
extends: .build-base
|
||||
@ -91,6 +101,17 @@ build-linux:
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-linux-qa:
|
||||
extends: .build-base
|
||||
only:
|
||||
- web
|
||||
script:
|
||||
- BUILD_TAGS="build_qa pmapi_qa" make build
|
||||
artifacts:
|
||||
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-ie-linux:
|
||||
extends: .build-base
|
||||
script:
|
||||
@ -124,6 +145,17 @@ build-darwin:
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-darwin-qa:
|
||||
extends: .build-darwin-base
|
||||
only:
|
||||
- web
|
||||
script:
|
||||
- BUILD_TAGS="build_qa pmapi_qa" make build
|
||||
artifacts:
|
||||
name: "bridge-darwin-qa-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-ie-darwin:
|
||||
extends: .build-darwin-base
|
||||
script:
|
||||
@ -155,6 +187,23 @@ build-windows:
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-windows-qa:
|
||||
extends: .build-windows-base
|
||||
only:
|
||||
- web
|
||||
script:
|
||||
# We need to install docker because qtdeploy builds for windows inside a docker container.
|
||||
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
|
||||
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
|
||||
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
|
||||
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
|
||||
- go mod download
|
||||
- TARGET_OS=windows BUILD_TAGS="build_qa pmapi_qa" make build
|
||||
artifacts:
|
||||
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-ie-windows:
|
||||
extends: .build-windows-base
|
||||
script:
|
||||
|
||||
18
BUILDS.md
18
BUILDS.md
@ -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.
|
||||
|
||||
## 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
|
||||
export MSYSTEM=
|
||||
```
|
||||
|
||||
|
||||
### Build Bridge
|
||||
* 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 `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
|
||||
In order to be able to run following commands please install the development dependencies:
|
||||
|
||||
355
Changelog.md
355
Changelog.md
@ -1,62 +1,261 @@
|
||||
# ProtonMail Bridge Changelog
|
||||
# ProtonMail Bridge and Import-Export app Changelog
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## Unreleased
|
||||
## [Bridge 1.5.4] Golden Gate
|
||||
|
||||
## [IE 0.2.x] Congo
|
||||
### 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
|
||||
|
||||
### Changed
|
||||
* 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.
|
||||
|
||||
|
||||
## [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)
|
||||
|
||||
### Fixed
|
||||
* GODT-752 Parsing message with empty addresses.
|
||||
* GODT-752 Parsing non-utf8 multipart/alternative message.
|
||||
* GODT-752 Parsing message with duplicate charset parameter.
|
||||
|
||||
|
||||
## [IE 1.1.0] Danube
|
||||
|
||||
### Fixed
|
||||
* GODT-703 Import-Export showed always at least one total message.
|
||||
* 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
|
||||
|
||||
### Added
|
||||
* GODT-682 Persistent anonymous API cookies for Import-Export.
|
||||
* GODT-357 Use go-message to make a better message parser.
|
||||
* GODT-720 Time measurement of progress for Import-Export.
|
||||
|
||||
### Changed
|
||||
* GODT-511 User agent format changed.
|
||||
* Unsilent errors reading mbox files.
|
||||
* GODT-692 QA build with option to change API URL by ENV variable.
|
||||
* GODT-704 User agent detected by fake IMAP extension instead of AUTH callback (some clients use LOGIN instead of AUTH).
|
||||
* GODT-695 Parallel upload for ProtonMail target.
|
||||
|
||||
### Removed
|
||||
* GODT-519 Unused AUTH scope parsing methods.
|
||||
|
||||
### Fixed
|
||||
* GODT-698 Use correct package type for signed PGP/Inline messages.
|
||||
* Generic bug report window title.
|
||||
* Fix missing check for unencrypted recipients during sending.
|
||||
* Version checking for catalina.
|
||||
* GODT-730 Limit maximal TLS version for Yahoo IMAP server.
|
||||
|
||||
|
||||
## [IE 1.0.x] Congo (v1.0.0 live 2020-09-08)
|
||||
|
||||
### Added
|
||||
* GODT-633 Persistent anonymous API cookies for better load balancing and abuse detection.
|
||||
* GODT-461 Add support for `\Deleted` flag.
|
||||
|
||||
### Changed
|
||||
* GODT-462 Pausing event loop while FETCHing to prevent EXPUNGE.
|
||||
* Wait for unilateral response to be delivered.
|
||||
* GODT-409 Set flags have to replace all flags.
|
||||
* 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.
|
||||
* Bumped various dependencies:
|
||||
* andybalholm/cascadia v1.1.0 -> v1.2.0
|
||||
* emersion/go-imap-specialuse 20161227184202-ba031ced6a62 -> 20200722111535-598ff00e4075
|
||||
* emersion/go-sasl 20191210011802-430746ea8b9b -> 20200509203442-7bfe0ed36a21
|
||||
* github.com/go-resty/resty/v2 v2.2.0 -> v2.3.0
|
||||
* github.com/golang/mock v1.4.3 -> v1.4.4
|
||||
* github.com/google/go-cmp v0.4.0 -> v0.5.1
|
||||
* github.com/hashicorp/go-multierror v1.0.0 -> v1.1.0
|
||||
* github.com/jaytaylor/html2text 20200220170450-61d9dc4d7195 -> 20200412013138-3577fbdbcff7
|
||||
* github.com/jhillyerd/enmime v0.8.0 -> v0.8.1
|
||||
* github.com/keybase/go-keychain 20200218013740-86d4642e4ce2 -> 20200502122510-cda31fe0c86d
|
||||
* github.com/logrusorgru/aurora 20200102142835-e9ef32dff381 -> v2.0.3+incompatible
|
||||
* github.com/miekg/dns v1.1.29 -> v1.1.30
|
||||
* github.com/nsf/jsondiff 20190712045011-8443391ee9b6 -> 20200515183724-f29ed568f4ce
|
||||
* github.com/sirupsen/logrus v1.4.2 -> v1.6.0
|
||||
* github.com/stretchr/testify v1.5.1 -> v1.6.1
|
||||
* github.com/therecipe/qt 20200126204426-5074eb6d8c41 -> 20200701200531-7f61353ee73e
|
||||
* github.com/urfave/cli v1.22.3 -> v1.22.4
|
||||
* golang.org/x/net 20200301022130-244492dfa37a -> 20200707034311-ab3426394381
|
||||
* golang.org/x/text v0.3.2 -> v0.3.3
|
||||
* Updated andybalholm/cascadia v1.1.0 -> v1.2.0.
|
||||
* Updated emersion/go-imap-specialuse 20161227184202-ba031ced6a62 -> 20200722111535-598ff00e4075.
|
||||
* Updated emersion/go-sasl 20191210011802-430746ea8b9b -> 20200509203442-7bfe0ed36a21.
|
||||
* Updated github.com/go-resty/resty/v2 v2.2.0 -> v2.3.0.
|
||||
* Updated github.com/golang/mock v1.4.3 -> v1.4.4.
|
||||
* Updated github.com/google/go-cmp v0.4.0 -> v0.5.1.
|
||||
* Updated github.com/hashicorp/go-multierror v1.0.0 -> v1.1.0.
|
||||
* Updated github.com/jaytaylor/html2text 20200220170450-61d9dc4d7195 -> 20200412013138-3577fbdbcff7.
|
||||
* Updated github.com/jhillyerd/enmime v0.8.0 -> v0.8.1.
|
||||
* Updated github.com/keybase/go-keychain 20200218013740-86d4642e4ce2 -> 20200502122510-cda31fe0c86d.
|
||||
* Updated github.com/logrusorgru/aurora 20200102142835-e9ef32dff381 -> v2.0.3+incompatible.
|
||||
* Updated github.com/miekg/dns v1.1.29 -> v1.1.30.
|
||||
* Updated github.com/nsf/jsondiff 20190712045011-8443391ee9b6 -> 20200515183724-f29ed568f4ce.
|
||||
* Updated github.com/sirupsen/logrus v1.4.2 -> v1.6.0.
|
||||
* Updated github.com/stretchr/testify v1.5.1 -> v1.6.1.
|
||||
* Updated github.com/therecipe/qt 20200126204426-5074eb6d8c41 -> 20200701200531-7f61353ee73e.
|
||||
* Updated github.com/urfave/cli v1.22.3 -> v1.22.4.
|
||||
* Updated golang.org/x/net 20200301022130-244492dfa37a -> 20200707034311-ab3426394381.
|
||||
* Updated golang.org/x/text v0.3.2 -> v0.3.3.
|
||||
* Set first-start to false in bridge, not in frontend.
|
||||
* GODT-400 Refactor sendingInfo.
|
||||
* GODT-380 Adding IE GUI to Bridge repo and building
|
||||
* BR: extend functionality of PopupDialog
|
||||
* BR: makefile APP_VERSION instead of BRIDGE_VERSION
|
||||
* BR: use common logs function for Qt
|
||||
* BR: change `go.progressDescription` to `string`
|
||||
* IE: Rounded button has fa-icon
|
||||
* IE: `Upgrade` → `Update`
|
||||
* IE: Moving `AccountModel` to `qt-common`
|
||||
* IE: Added `ReportBug` to `internal/importexport`
|
||||
* IE: Added event watch in GUI
|
||||
* IE: Removed `onLoginFinished`
|
||||
* Structure for transfer rules in QML
|
||||
* GODT-513 Update routes to API v4.
|
||||
* GODT-551 Do not ignore errors during message flagging.
|
||||
* GODT-380 Adding IE GUI to Bridge repo and building.
|
||||
* BR: extend functionality of PopupDialog.
|
||||
* BR: makefile APP_VERSION instead of BRIDGE_VERSION.
|
||||
* BR: use common logs function for Qt.
|
||||
* BR: change `go.progressDescription` to `string`.
|
||||
* IE: Rounded button has fa-icon.
|
||||
* IE: `Upgrade` → `Update`.
|
||||
* IE: Moving `AccountModel` to `qt-common`.
|
||||
* IE: Added `ReportBug` to `internal/importexport`.
|
||||
* IE: Added event watch in GUI.
|
||||
* IE: Removed `onLoginFinished`.
|
||||
* Structure for transfer rules in QML.
|
||||
* GODT-213 Convert panics from message parser to error.
|
||||
* GODT-585 Do not allow deleting messages from All Mail.
|
||||
|
||||
### 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-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.
|
||||
|
||||
|
||||
## [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
|
||||
* GODT-554 Detect and notify about "bad certificate" IMAP TLS error.
|
||||
@ -116,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.
|
||||
* Correctly handle failure to unlock single key.
|
||||
* 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-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.
|
||||
@ -125,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.
|
||||
|
||||
|
||||
## [v1.2.8] Donghai-fix-append (beta 2020-06-XXX)
|
||||
## [Bridge 1.2.8] Donghai-fix-append (beta 2020-06-XXX)
|
||||
|
||||
### Changed
|
||||
* GODT-396 reduce number of EXISTS calls.
|
||||
@ -134,7 +333,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
### Fixed
|
||||
* 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
|
||||
* IMAP extension MOVE with UIDPLUS support.
|
||||
@ -157,7 +356,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* 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
|
||||
* GODT-145 Support drafts.
|
||||
@ -212,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).
|
||||
|
||||
|
||||
## [v1.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.
|
||||
## [Bridge 1.2.5] Charles - live (2020-03-11) beta (from 2020-02-10)
|
||||
|
||||
### Added
|
||||
* GODT-112 Migration of preferences from c10 to c11.
|
||||
@ -263,13 +456,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Separated IMAP to store and IMAP.
|
||||
* Store is responsible for everything about db and calls to pmapi, including event loop, sync, address mode.
|
||||
* IMAP is responsible only for IMAP interfaces.
|
||||
* 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)
|
||||
* Changing address mode is not destroying database, only buckets with IDs mapping (keeping metadata for account)
|
||||
* 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).
|
||||
* 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.
|
||||
* 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 reading operations (user can list mailboxes even before first sync is done)
|
||||
* 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 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.
|
||||
* Avoid relying on counts API endpoint; use event counts as much as possible.
|
||||
@ -279,8 +472,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* 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.
|
||||
* 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)
|
||||
* DB keeps track of address mode (split/combined)
|
||||
* 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).
|
||||
* Event loop starts as soon as user is initialised (i.e. logged in), not just when imap is connected.
|
||||
* Use pmapi v1.0.13.
|
||||
* Logout user if initialisation fails.
|
||||
@ -288,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).
|
||||
|
||||
### 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".
|
||||
* #1056 Changing mailbox password sometimes didn't log out user.
|
||||
* #1066 Split address mode can not work when credentials store is cleared.
|
||||
@ -303,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.
|
||||
|
||||
|
||||
## [v1.2.4] Brooklyn beta (2019-12-16)
|
||||
## [Bridge 1.2.4] Brooklyn beta (2019-12-16)
|
||||
|
||||
### Added
|
||||
* #976: fix slow authentication.
|
||||
@ -318,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.
|
||||
* 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
|
||||
* #963 report first-start metric with bridge version.
|
||||
@ -348,17 +545,17 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* 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
|
||||
* User compare case insensitive.
|
||||
|
||||
## [v1.2.1] - beta and live 2019-09-05
|
||||
## [Bridge 1.2.1] - beta and live 2019-09-05
|
||||
|
||||
### Changed
|
||||
* #924 fix start of bridge without internet connection.
|
||||
|
||||
## [v1.2.0] - beta 2019-08-22
|
||||
## [Bridge 1.2.0] - beta 2019-08-22
|
||||
|
||||
### Added
|
||||
* #903 added http.Client timeout to not hang out forever.
|
||||
@ -458,7 +655,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* 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
|
||||
* #841 assume text/plain during sending e-mails when missing content type.
|
||||
@ -490,7 +687,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* 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
|
||||
* Fix custom message format.
|
||||
@ -504,7 +701,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Only one crash from second instance.
|
||||
* 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
|
||||
* Address with port to IMAP debug.
|
||||
@ -525,7 +722,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
### Removed
|
||||
* #750 Synchronization after 450 messages.
|
||||
|
||||
## [v1.1.3] - 2019-03-04
|
||||
## [Bridge 1.1.3] - 2019-03-04
|
||||
|
||||
### Added
|
||||
* Sentry crash reporting in main.
|
||||
@ -537,13 +734,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* #720 sync every 3 pages.
|
||||
* #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
|
||||
* #512 fail on unknown charset.
|
||||
* #729 #733 visitor for MIME parsing.
|
||||
|
||||
## [v1.1.1] - 2019-02-11
|
||||
## [Bridge 1.1.1] - 2019-02-11
|
||||
### Added
|
||||
* #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]`.
|
||||
@ -596,7 +793,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* SMTP stays authenticated after sent message.
|
||||
* Reduce memory, processor and number of API calls.
|
||||
|
||||
## [v1.1.0] - 2018-10-22
|
||||
## [Bridge 1.1.0] - 2018-10-22
|
||||
|
||||
### Removed
|
||||
* `go-pmapi.Config.ClientSecret`.
|
||||
@ -672,11 +869,11 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Additional synchronization of mail database.
|
||||
|
||||
|
||||
## [v1.0.6 silent] - 2018-08-23
|
||||
## [Bridge 1.0.6 silent] - 2018-08-23
|
||||
### Added
|
||||
* New svg icon in linux package.
|
||||
|
||||
## [v1.0.6] - 2018-08-09
|
||||
## [Bridge 1.0.6] - 2018-08-09
|
||||
|
||||
### Added
|
||||
* `backend.GetUserSettings()`.
|
||||
@ -705,7 +902,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Frequent Thunderbird timeout.
|
||||
* SMTP requests not case-sensitive.
|
||||
|
||||
## [v1.0.5] - 2018-07-12
|
||||
## [Bridge 1.0.5] - 2018-07-12
|
||||
|
||||
### Added
|
||||
* UpdateCurrentAgent from lastMailClient.
|
||||
@ -737,7 +934,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Fixed 7bit MIME issue while sending.
|
||||
|
||||
|
||||
## [v1.0.4] - 2018-05-15
|
||||
## [Bridge 1.0.4] - 2018-05-15
|
||||
|
||||
### Changed
|
||||
* Version files available at both download and static.
|
||||
@ -758,11 +955,11 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* 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.
|
||||
|
||||
### Bugs fixed
|
||||
### Fixed bugs
|
||||
* Support accounts with same user names.
|
||||
* Support sending vCalendar event.
|
||||
|
||||
## [v1.0.3] - 2018-03-26
|
||||
## [Bridge 1.0.3] - 2018-03-26
|
||||
* All from silent updates plus following.
|
||||
|
||||
### Changed
|
||||
@ -797,7 +994,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Remove firewall error message.
|
||||
|
||||
|
||||
## [v1.0.2] - 2018-03-12
|
||||
## [Bridge 1.0.2] - 2018-03-12
|
||||
* All from silent updates plus following.
|
||||
|
||||
### Added
|
||||
@ -819,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
|
||||
* More similar look of window title bar to Windows 10 style.
|
||||
@ -843,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
|
||||
* Fixed bug with parsing address list (CC became BCC).
|
||||
|
||||
|
||||
|
||||
## [v1.0.1] - 2017-12-20
|
||||
## [Bridge 1.0.1] - 2017-12-20
|
||||
|
||||
### Added
|
||||
* When current log file is more than 10MB open new one, checked every 15min.
|
||||
@ -882,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
|
||||
* Encoding support of message body, title items, attachment name, for all standard charsets.
|
||||
|
||||
48
Makefile
48
Makefile
@ -9,19 +9,22 @@ TARGET_OS?=${GOOS}
|
||||
## Build
|
||||
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
|
||||
|
||||
BRIDGE_APP_VERSION?=1.4.0-git
|
||||
IE_APP_VERSION?=1.0.0-git
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=1.5.4-git
|
||||
IE_APP_VERSION?=1.2.3-git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
SRC_ICO:=logo.ico
|
||||
SRC_ICNS:=Bridge.icns
|
||||
SRC_SVG:=logo.svg
|
||||
TGT_ICNS:=Bridge.icns
|
||||
EXE_NAME:=proton-bridge
|
||||
ifeq "${TARGET_CMD}" "Import-Export"
|
||||
APP_VERSION:=${IE_APP_VERSION}
|
||||
SRC_ICO:=ie.ico
|
||||
SRC_ICNS:=ie.icns
|
||||
SRC_SVG:=ie.svg
|
||||
TGT_ICNS:=ImportExport.icns
|
||||
EXE_NAME:=proton-ie
|
||||
endif
|
||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
@ -39,30 +42,40 @@ BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
|
||||
|
||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||
ICO_FILES:=
|
||||
EXE:=$(shell basename ${CURDIR})
|
||||
|
||||
DIRNAME:=$(shell basename ${CURDIR})
|
||||
EXE:=${EXE_NAME}
|
||||
EXE_QT:=${DIRNAME}
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
EXE:=${EXE}.exe
|
||||
EXE_QT:=${EXE_QT}.exe
|
||||
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
|
||||
endif
|
||||
ifeq "${TARGET_OS}" "darwin"
|
||||
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
|
||||
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
|
||||
EXE_QT_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE_QT}
|
||||
|
||||
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
|
||||
ifeq "${TARGET_CMD}" "Import-Export"
|
||||
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
|
||||
endif
|
||||
|
||||
ifdef QT_API
|
||||
VENDOR_TARGET:=prepare-vendor update-qt-docs
|
||||
else
|
||||
VENDOR_TARGET=update-vendor
|
||||
endif
|
||||
|
||||
build: ${TGZ_TARGET}
|
||||
build-ie:
|
||||
TARGET_CMD=Import-Export $(MAKE) build
|
||||
|
||||
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:
|
||||
TARGET_CMD=Import-Export $(MAKE) build-nogui
|
||||
@ -77,12 +90,16 @@ ${DEPLOY_DIR}/linux: ${EXE_TARGET}
|
||||
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
|
||||
|
||||
${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 LICENSE ${DARWINAPP_CONTENTS}/Resources/
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}"
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}"
|
||||
|
||||
${DEPLOY_DIR}/windows: ${EXE_TARGET}
|
||||
cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico
|
||||
@ -95,11 +112,12 @@ ifneq "${GOOS}" "${TARGET_OS}"
|
||||
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}
|
||||
cp cmd/${TARGET_CMD}/main.go .
|
||||
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
|
||||
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
|
||||
|
||||
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
|
||||
@ -111,7 +129,7 @@ icon_windows.syso: icon.rc logo.ico
|
||||
|
||||
|
||||
## Rules for therecipe/qt
|
||||
.PHONY: prepare-vendor update-vendor
|
||||
.PHONY: prepare-vendor update-vendor update-qt-docs
|
||||
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
|
||||
|
||||
# vendor folder will be deleted by gomod hence we cache the big repo
|
||||
@ -136,6 +154,8 @@ prepare-vendor:
|
||||
update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
|
||||
${LINKCMD}
|
||||
|
||||
update-qt-docs:
|
||||
go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API)
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated
|
||||
@ -196,16 +216,19 @@ coverage: test
|
||||
|
||||
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/transfer PanicHandler,ClientManager > 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/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/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/pmapi Client > pkg/pmapi/mocks/mocks.go
|
||||
|
||||
lint: lint-golang lint-license
|
||||
lint: lint-golang lint-license lint-changelog
|
||||
|
||||
lint-license:
|
||||
./utils/missing_license.sh check
|
||||
|
||||
lint-changelog:
|
||||
./utils/changelog_linter.sh
|
||||
|
||||
lint-golang:
|
||||
which golangci-lint || $(MAKE) install-linter
|
||||
golangci-lint run ./...
|
||||
@ -264,7 +287,6 @@ run-ie-qt:
|
||||
run-ie-nogui:
|
||||
TARGET_CMD=Import-Export $(MAKE) run-nogui
|
||||
|
||||
|
||||
clean-frontend-qt:
|
||||
$(MAKE) -C internal/frontend/qt -f Makefile.local clean
|
||||
clean-frontend-qt-ie:
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
# ProtonMail Bridge and Import Export app
|
||||
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 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).
|
||||
|
||||
|
||||
@ -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,
|
||||
check the results.
|
||||
|
||||
More details [on the public website](https://protonmail.com/import-export).
|
||||
|
||||
## Keychain
|
||||
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
|
||||
|
||||
@ -21,9 +21,11 @@ import (
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/cmd"
|
||||
"github.com/ProtonMail/proton-bridge/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
@ -36,6 +38,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// cacheVersion is used for cache files such as lock, or preferences.
|
||||
// Different number will drop old files and create new ones.
|
||||
cacheVersion = "c11"
|
||||
|
||||
appName = "importExport"
|
||||
appNameDash = "import-export-app"
|
||||
)
|
||||
@ -58,7 +64,7 @@ func main() {
|
||||
// IMPORTANT: ***Read the comments before CHANGING the order ***
|
||||
func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
// We need to have config instance to setup a logs, panic handler, etc ...
|
||||
cfg := config.New(appName, constants.Version, constants.Revision, "")
|
||||
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
|
||||
|
||||
// We want to know about any problem. Our PanicHandler calls sentry which is
|
||||
// not dependent on anything else. If that fails, it tries to create crash
|
||||
@ -132,6 +138,16 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
// implementation depending on whether build flag pmapi_prod is used or not.
|
||||
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
|
||||
|
||||
pref := preferences.New(cfg)
|
||||
|
||||
// Cookies must be persisted across restarts.
|
||||
jar, err := cookies.NewCookieJar(pref)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Warn("Could not create cookie jar")
|
||||
} else {
|
||||
cm.SetCookieJar(jar)
|
||||
}
|
||||
|
||||
importexportInstance := importexport.New(cfg, panicHandler, eventListener, cm, credentialsStore)
|
||||
|
||||
// Decide about frontend mode before initializing rest of import-export.
|
||||
|
||||
26
go.mod
26
go.mod
@ -6,24 +6,24 @@ go 1.13
|
||||
// They are in a separate require block to highlight this.
|
||||
require (
|
||||
github.com/docker/docker-credential-helpers v0.6.3
|
||||
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
|
||||
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
|
||||
github.com/Masterminds/semver/v3 v3.1.0
|
||||
github.com/ProtonMail/go-appdir v1.1.0
|
||||
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
|
||||
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/gopenpgp/v2 v2.0.1
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
|
||||
github.com/andybalholm/cascadia v1.2.0
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/cucumber/godog v0.8.1
|
||||
@ -34,23 +34,22 @@ require (
|
||||
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-unselect v0.0.0-20171113212723-b985794e5f26
|
||||
github.com/emersion/go-mbox v1.0.0
|
||||
github.com/emersion/go-message v0.11.1
|
||||
github.com/emersion/go-mbox v1.0.2
|
||||
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-smtp v0.14.0
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
|
||||
github.com/fatih/color v1.9.0
|
||||
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/golang/mock v1.4.4
|
||||
github.com/google/go-cmp v0.5.1
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/go-delve/delve v1.4.1 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
|
||||
github.com/jhillyerd/enmime v0.8.1
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
@ -60,13 +59,11 @@ require (
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
|
||||
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/psampaz/go-mod-outdated v0.6.0 // indirect
|
||||
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/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22 // indirect
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22 // indirect
|
||||
github.com/twinj/uuid v1.0.0 // indirect
|
||||
github.com/urfave/cli v1.22.4
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
@ -77,8 +74,7 @@ require (
|
||||
|
||||
replace (
|
||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||
github.com/emersion/go-imap => github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843
|
||||
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
|
||||
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474
|
||||
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
|
||||
)
|
||||
|
||||
252
go.sum
252
go.sum
@ -1,10 +1,16 @@
|
||||
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/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/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/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/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
|
||||
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
|
||||
github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8 h1:u1j0xLTrCHpNS40B6m4Sv3IVUz5m9jt+AnTIopT3IgM=
|
||||
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/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
|
||||
@ -13,33 +19,42 @@ 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-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474 h1:D0RwDtkBw0Gt7hmbb1ivdEulplJAwu1i2jzh4HM45fo=
|
||||
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/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
|
||||
github.com/ProtonMail/go-rfc5322 v0.2.1 h1:J2PHusboDAYUfE+uBfoJnKZPbnVmzK1zXw6dQrgV8yE=
|
||||
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/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/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
|
||||
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/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/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/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/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
|
||||
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
|
||||
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY=
|
||||
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/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/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/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
|
||||
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=
|
||||
@ -52,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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
|
||||
@ -64,134 +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-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-mbox v1.0.0 h1:HN6aKbyqmgIfK9fS/gen+NRr2wXLSxZXWfdAIAnzQPc=
|
||||
github.com/emersion/go-mbox v1.0.0/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
||||
github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac=
|
||||
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
|
||||
github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
||||
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
|
||||
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-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/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/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/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/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/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
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/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
|
||||
github.com/go-delve/delve v1.4.1 h1:kZs0umEv+VKnK84kY9/ZXWrakdLTeRTyYjFdgLelZCQ=
|
||||
github.com/go-delve/delve v1.4.1/go.mod h1:vmy6iObn7zg8FQ5KOCIe6TruMNsqpoZO8uMiRea+97k=
|
||||
github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
|
||||
github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
|
||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/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/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/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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
|
||||
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-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
|
||||
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/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/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
|
||||
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
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/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jhillyerd/enmime v0.8.1 h1:Kz4xj3sJJ4Ju8e+w/7v9H4Matv5ijPgv7UkhPf+C15I=
|
||||
github.com/jhillyerd/enmime v0.8.1/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
|
||||
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/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/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
|
||||
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.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/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/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/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/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/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
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/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.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/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
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/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/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mitchellh/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/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/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/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
|
||||
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
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/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
|
||||
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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/psampaz/go-mod-outdated v0.6.0 h1:DXS6rdsz4rpezbPsckQflqrYSEBvsF5GAmUWP+UvnQo=
|
||||
github.com/psampaz/go-mod-outdated v0.6.0/go.mod h1:r78NYWd1z+F9Zdsfy70svgXOz363B08BWnTyFSgEESs=
|
||||
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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
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/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.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
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.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
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/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
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/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -201,86 +262,99 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
||||
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
|
||||
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
||||
github.com/therecipe/qt v0.0.0-20200603231648-26cdb75b6f22 h1:UrNr8EZueA1eREFmG5gVHBeeOuwW2GbzI9VfdB5uK+c=
|
||||
github.com/therecipe/qt/internal/binding/files/docs v0.0.0-20191019224306-1097424d656c h1:/VhcwU7WuFEVgDHZ9V8PIYAyYqQ6KNxFUjBMOf2aFZM=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22 h1:FumuOkCw78iheUI3eIYhAgtsj/0HQBAib/jXk1cslJw=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22 h1:aYzTBQ/hC6FtbaRnyylxlhbSGMPnyD5lAzVO3Ae6emA=
|
||||
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
|
||||
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
|
||||
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
|
||||
github.com/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/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/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
|
||||
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
|
||||
golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
|
||||
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-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-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-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-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-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-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
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/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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/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-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
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-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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
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/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
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.4/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at Fri 04 Sep 2020 01:57:36 PM CEST. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Fri Nov 27 09:23:06 CET 2020. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-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-delve/delve;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameshoulahan/go-imap;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/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/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/psampaz/go-mod-outdated;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/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;"
|
||||
|
||||
@ -15,17 +15,16 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./release-notes.sh at 'Fri 04 Sep 2020 01:57:36 PM CEST'. DO NOT EDIT.
|
||||
// Code generated by ./release-notes.sh at 'Wed Dec 9 05:55:04 PM CET 2020'. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const ReleaseNotes = `• Improvements to Alternative Routing: Version two of this feature is now more resilient to unstable internet connections, which results in a smoother experience using this feature. Also includes fixes to previous implementation of Alternative Routing when first starting the application or when turning it off.
|
||||
• Email parsing improvements: Improved detection of email encodings embedded in html/xml in addition to message header; add a fallback option if encoding is not specified and decoding as UTF8 fails (ISO-8859-1) ; tweaked logic of parsing "References" header.
|
||||
• User interaction improvements: Some smaller improvements in specific cases to make the interaction with Proton Bridge clearer for the user
|
||||
• Code updates & maintenance: Migrated to GopenPGP v2, updates to GoIMAPv1, increased bbolt version to 1.3.5 and various improvements regarding extensibility and maintainability for upcoming work.
|
||||
• General stability improvements: Improvements to the behavior of the application under various unstable internet conditions.
|
||||
const ReleaseNotes = `• Support read confirmations
|
||||
• Adding GPLv3 licence button to the GUI
|
||||
• Improved testing
|
||||
`
|
||||
|
||||
const ReleaseFixedBugs = `• Fixed a slew of smaller bugs and some conditions which could cause the application to crash.
|
||||
• The full changelog can be found at https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
|
||||
const ReleaseFixedBugs = `• AppleMail crashes (timestamp related)
|
||||
• Encoding errors
|
||||
• Installation issues on linux
|
||||
`
|
||||
|
||||
@ -22,7 +22,8 @@ import (
|
||||
"runtime"
|
||||
|
||||
"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/urfave/cli"
|
||||
)
|
||||
@ -51,10 +52,19 @@ var (
|
||||
|
||||
// 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) {
|
||||
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")
|
||||
}
|
||||
raven.SetRelease(constants.Revision)
|
||||
|
||||
filterProcessSerialNumberFromArgs()
|
||||
filterRestartNumberFromArgs()
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
@ -92,6 +93,8 @@ type PanicHandler struct {
|
||||
|
||||
// HandlePanic should be called in defer to ensure restart of app after error.
|
||||
func (ph *PanicHandler) HandlePanic() {
|
||||
sentry.SkipDuringUnwind()
|
||||
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
|
||||
@ -40,7 +40,6 @@ const (
|
||||
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
|
||||
UpgradeApplicationEvent = "upgradeApplication"
|
||||
TLSCertIssue = "tlsCertPinningIssue"
|
||||
IMAPTLSBadCert = "imapTLSBadCert"
|
||||
|
||||
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
|
||||
LogoutEventTimeout = 3 * time.Minute
|
||||
|
||||
@ -79,7 +79,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
return
|
||||
}
|
||||
|
||||
_, err = client.Auth2FA(twoFactor, auth)
|
||||
err = client.Auth2FA(twoFactor, auth)
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
|
||||
@ -184,9 +184,17 @@ func (f *frontendCLI) setTransferRules(t *transfer.Transfer) bool {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printTransferProgress(progress *transfer.Progress) {
|
||||
failed, imported, exported, added, total := progress.GetCounts()
|
||||
if total != 0 {
|
||||
f.Println(fmt.Sprintf("Progress update: %d (%d / %d) / %d, failed: %d", imported, exported, added, total, failed))
|
||||
counts := progress.GetCounts()
|
||||
if counts.Total != 0 {
|
||||
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() {
|
||||
|
||||
@ -126,7 +126,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
return
|
||||
}
|
||||
|
||||
_, err = client.Auth2FA(twoFactor, auth)
|
||||
err = client.Auth2FA(twoFactor, auth)
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
|
||||
@ -102,7 +102,7 @@ Item {
|
||||
Row {
|
||||
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 {
|
||||
id:credits
|
||||
@ -114,6 +114,20 @@ Item {
|
||||
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"}
|
||||
|
||||
ClickIconText {
|
||||
|
||||
@ -354,7 +354,7 @@ Window {
|
||||
} else {
|
||||
return qsTr('A new version of Bridge is available.<br>
|
||||
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
|
||||
You can continue with the update or download and install new version manually from<br><br>
|
||||
You can continue with the update or download and install the new version manually from<br><br>
|
||||
<a href="%3">%3</a>',
|
||||
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
||||
}
|
||||
|
||||
@ -48,6 +48,7 @@ Item {
|
||||
text : qsTr("Clear", "clickable link next to clear cache button in settings")
|
||||
color: Style.main.text
|
||||
font {
|
||||
family : cacheClear.font.family // use default font, not font-awesome
|
||||
pointSize : Style.settings.fontSize * Style.pt
|
||||
underline : true
|
||||
}
|
||||
@ -66,6 +67,7 @@ Item {
|
||||
text : qsTr("Clear", "clickable link next to clear keychain button in settings")
|
||||
color: Style.main.text
|
||||
font {
|
||||
family : cacheKeychain.font.family // use default font, not font-awesome
|
||||
pointSize : Style.settings.fontSize * Style.pt
|
||||
underline : true
|
||||
}
|
||||
@ -125,6 +127,7 @@ Item {
|
||||
text : qsTr("Change", "clickable link next to change ports button in settings")
|
||||
color: Style.main.text
|
||||
font {
|
||||
family : changePort.font.family // use default font, not font-awesome
|
||||
pointSize : Style.settings.fontSize * Style.pt
|
||||
underline : true
|
||||
}
|
||||
|
||||
@ -237,13 +237,6 @@ Item {
|
||||
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"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -276,6 +276,10 @@ Item {
|
||||
winMain.dialogExport.hide()
|
||||
}
|
||||
}
|
||||
|
||||
onUpdateFinished : {
|
||||
winMain.dialogUpdate.finished(hasError)
|
||||
}
|
||||
}
|
||||
|
||||
function folderIcon(folderName, folderType) { // translations
|
||||
|
||||
@ -217,7 +217,10 @@ Dialog {
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
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 (
|
||||
go.progressDescription == gui.enums.progressInit ||
|
||||
(go.progress==0 && go.description=="")
|
||||
@ -450,7 +453,6 @@ Dialog {
|
||||
errorPopup.hide()
|
||||
}
|
||||
onClickedNo : {
|
||||
go.resumeProcess()
|
||||
errorPopup.hide()
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,9 +279,8 @@ Dialog {
|
||||
titleTo : root.address
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Column {
|
||||
id: masterImportSettings
|
||||
height: 150 // fixme
|
||||
anchors {
|
||||
right : parent.right
|
||||
left : parent.left
|
||||
@ -291,45 +290,47 @@ Dialog {
|
||||
rightMargin : Style.main.leftMargin
|
||||
bottomMargin : Style.main.bottomMargin
|
||||
}
|
||||
color: Style.dialog.background
|
||||
|
||||
Text {
|
||||
id: labelMasterImportSettings
|
||||
text: qsTr("Master import settings:")
|
||||
spacing: Style.main.bottomMargin
|
||||
|
||||
font {
|
||||
bold: true
|
||||
family: Style.fontawesome.name
|
||||
pointSize: Style.main.fontSize * Style.pt
|
||||
}
|
||||
color: Style.main.text
|
||||
Row {
|
||||
spacing: masterImportSettings.width - labelMasterImportSettings.width - resetSourceButton.width
|
||||
|
||||
InfoToolTip {
|
||||
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."
|
||||
)
|
||||
anchors {
|
||||
left: parent.right
|
||||
bottom: parent.bottom
|
||||
leftMargin : Style.dialog.leftMargin
|
||||
Text {
|
||||
id: labelMasterImportSettings
|
||||
text: qsTr("Master import settings:")
|
||||
|
||||
font {
|
||||
bold: true
|
||||
family: Style.fontawesome.name
|
||||
pointSize: Style.main.fontSize * Style.pt
|
||||
}
|
||||
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
|
||||
ClickIconText {
|
||||
anchors {
|
||||
right: parent.right
|
||||
bottom: labelMasterImportSettings.bottom
|
||||
}
|
||||
text:qsTr("Reset all settings to default")
|
||||
iconText: Style.fa.refresh
|
||||
textColor: Style.main.textBlue
|
||||
onClicked: {
|
||||
go.resetSource()
|
||||
root.decrementCurrentIndex()
|
||||
timer.start()
|
||||
// Reset all to default
|
||||
ClickIconText {
|
||||
id: resetSourceButton
|
||||
text:qsTr("Reset all settings to default")
|
||||
iconText: Style.fa.refresh
|
||||
textColor: Style.main.textBlue
|
||||
onClicked: {
|
||||
go.resetSource()
|
||||
root.decrementCurrentIndex()
|
||||
timer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -348,49 +349,59 @@ Dialog {
|
||||
|
||||
InlineDateRange {
|
||||
id: globalDateRange
|
||||
anchors {
|
||||
left : parent.left
|
||||
top : line.bottom
|
||||
topMargin : Style.dialog.topMargin
|
||||
}
|
||||
}
|
||||
|
||||
// Add global label (inline)
|
||||
InlineLabelSelect {
|
||||
id: globalLabels
|
||||
anchors {
|
||||
left : parent.left
|
||||
top : globalDateRange.bottom
|
||||
topMargin : Style.dialog.topMargin
|
||||
}
|
||||
//labelWidth : globalDateRange.labelWidth
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Row {
|
||||
spacing: Style.dialog.spacing
|
||||
anchors{
|
||||
bottom : parent.bottom
|
||||
right : parent.right
|
||||
CheckBoxLabel {
|
||||
id: importEncrypted
|
||||
text: qsTr("Import encrypted emails as they are")
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
bottomMargin: Style.dialog.fontSize/1.8
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRounded {
|
||||
id: buttonCancelThree
|
||||
fa_icon : Style.fa.times
|
||||
text : qsTr("Cancel", "todo")
|
||||
color_main : Style.dialog.textBlue
|
||||
onClicked : root.cancel()
|
||||
InfoToolTip {
|
||||
anchors {
|
||||
verticalCenter: importEncrypted.verticalCenter
|
||||
}
|
||||
info: qsTr("When this option is enabled, encrypted emails will be imported as ciphertext. Otherwise, such messages will be skipped.", "todo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
// Buttons
|
||||
Row {
|
||||
spacing: Style.dialog.spacing
|
||||
anchors {
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
rightMargin: Style.main.leftMargin
|
||||
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
|
||||
visible: fails > 0
|
||||
color : Style.main.textRed
|
||||
font.family: Style.fontawesome.name
|
||||
font.pointSize: Style.main.fontSize * Style.pt
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: Style.fa.exclamation_circle + " " + (
|
||||
fails == 1 ?
|
||||
qsTr("%1 message failed to be imported").arg(fails) :
|
||||
qsTr("%1 messages failed to be imported").arg(fails)
|
||||
)
|
||||
|
||||
Text {
|
||||
color: Style.main.textRed
|
||||
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
|
||||
@ -575,16 +598,27 @@ Dialog {
|
||||
anchors.centerIn : finalReport
|
||||
spacing : Style.dialog.heightSeparator
|
||||
|
||||
Text {
|
||||
text: go.progressDescription!="" ? qsTr("Import failed: %1").arg(go.progressDescription) : Style.fa.check_circle + " " + qsTr("Import completed successfully")
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen
|
||||
font.bold : true
|
||||
font.family: Style.fontawesome.name
|
||||
|
||||
Text {
|
||||
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: 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
|
||||
textFormat: Text.RichText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
@ -773,11 +807,6 @@ Dialog {
|
||||
errorPopup.hide()
|
||||
}
|
||||
onClickedNo : {
|
||||
if (errorPopup.msgID == "ask_send_report") {
|
||||
errorPopup.hide()
|
||||
return
|
||||
}
|
||||
go.resumeProcess()
|
||||
errorPopup.hide()
|
||||
}
|
||||
|
||||
@ -1008,7 +1037,7 @@ Dialog {
|
||||
)
|
||||
break
|
||||
case DialogImport.Page.Progress:
|
||||
go.startImport(root.address)
|
||||
go.startImport(root.address, importEncrypted.checked)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ Item {
|
||||
)
|
||||
width: wrapper.width
|
||||
color : Style.transparent
|
||||
Text {
|
||||
AccessibleText {
|
||||
id: aboutText
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
@ -82,8 +82,8 @@ Item {
|
||||
}
|
||||
color: Style.main.textDisabled
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
font.family : Style.fontawesome.name
|
||||
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n"+Style.fa.copyright + " 2020 Proton Technologies AG"
|
||||
font.pointSize : Style.main.fontSize * Style.pt
|
||||
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 {
|
||||
id: releaseNotes
|
||||
|
||||
@ -45,7 +45,7 @@ Row {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -274,15 +274,15 @@ Window {
|
||||
}
|
||||
} else {
|
||||
if (go.goos=="linux") {
|
||||
return qsTr('New version of %1 is available.<br>
|
||||
return qsTr('A new version of %1 is available.<br>
|
||||
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
|
||||
Use your package manager to update or download and install new version manually from<br><br>
|
||||
Use your package manager to update or download and install new the version manually from<br><br>
|
||||
<a href="%4">%4</a>',
|
||||
"Message for update in Linux").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
||||
} else {
|
||||
return qsTr('New version of %1 is available.<br>
|
||||
return qsTr('A new version of %1 is available.<br>
|
||||
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
|
||||
You can continue with update or download and install new version manually from<br><br>
|
||||
You can continue with update or download and install new the version manually from<br><br>
|
||||
<a href="%4">%4</a>',
|
||||
"Message for update in Win/Mac").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
|
||||
}
|
||||
|
||||
@ -42,34 +42,50 @@ ComboBox {
|
||||
root.below = popup.y>0
|
||||
}
|
||||
|
||||
contentItem : Text {
|
||||
contentItem : Row {
|
||||
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
|
||||
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
|
||||
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: {
|
||||
if (view.currentIndex >= 0) {
|
||||
if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels")
|
||||
|
||||
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("Add/Remove labels")
|
||||
return view.currentItem.folderName
|
||||
}
|
||||
if (root.isFolderType) return qsTr("No folder selected")
|
||||
return qsTr("No labels selected")
|
||||
|
||||
@ -44,6 +44,7 @@ Item {
|
||||
text : qsTr("Clear")
|
||||
color: Style.main.text
|
||||
font {
|
||||
family : cacheKeychain.font.family // use default font, not font-awesome
|
||||
pointSize : Style.settings.fontSize * Style.pt
|
||||
underline : true
|
||||
}
|
||||
|
||||
@ -106,6 +106,7 @@ Rectangle {
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: mainText
|
||||
cursorShape: mainText.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ Window {
|
||||
|
||||
color : "transparent"
|
||||
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
|
||||
title : "ProtonMail Bridge - Bug report"
|
||||
title : "Bug report"
|
||||
visible : false
|
||||
|
||||
WindowTitleBar {
|
||||
@ -84,7 +84,7 @@ Window {
|
||||
height: content.height - (
|
||||
(clientVersion.visible ? clientVersion.height + Style.dialog.fontSize : 0) +
|
||||
userAddress.height + Style.dialog.fontSize +
|
||||
securityNote.contentHeight + Style.dialog.fontSize +
|
||||
securityNoteText.contentHeight + Style.dialog.fontSize +
|
||||
cancelButton.height + Style.dialog.fontSize
|
||||
)
|
||||
clip: true
|
||||
@ -215,7 +215,7 @@ Window {
|
||||
}
|
||||
|
||||
// Note
|
||||
AccessibleText {
|
||||
Row {
|
||||
id: securityNote
|
||||
anchors {
|
||||
left: parent.left
|
||||
@ -223,14 +223,32 @@ Window {
|
||||
top: userAddress.bottom
|
||||
topMargin: Style.dialog.fontSize
|
||||
}
|
||||
wrapMode: Text.Wrap
|
||||
color: Style.dialog.text
|
||||
font.pointSize : Style.dialog.fontSize * Style.pt
|
||||
text:
|
||||
"<span style='font-family: " + Style.fontawesome.name + "'>" + Style.fa.exclamation_triangle + "</span> " +
|
||||
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")
|
||||
|
||||
Text {
|
||||
id: securityNoteIcon
|
||||
font {
|
||||
pointSize : Style.dialog.fontSize * Style.pt
|
||||
family : Style.fontawesome.name
|
||||
}
|
||||
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
|
||||
|
||||
@ -122,7 +122,6 @@ Window {
|
||||
ListElement { title: "Minimize this" }
|
||||
ListElement { title: "SendAlertPopup" }
|
||||
ListElement { title: "TLSCertError" }
|
||||
ListElement { title: "IMAPCertError" }
|
||||
}
|
||||
|
||||
ListView {
|
||||
@ -209,9 +208,6 @@ Window {
|
||||
case "TLSCertError" :
|
||||
go.showCertIssue()
|
||||
break;
|
||||
case "IMAPCertError" :
|
||||
go.showIMAPCertTroubleshoot()
|
||||
break;
|
||||
default :
|
||||
console.log("Not implemented " + data)
|
||||
}
|
||||
@ -314,7 +310,6 @@ Window {
|
||||
signal failedAutostartCode(string code)
|
||||
|
||||
signal showCertIssue()
|
||||
signal showIMAPCertTroubleshoot()
|
||||
|
||||
signal updateFinished(bool hasError)
|
||||
|
||||
|
||||
@ -840,6 +840,8 @@ Window {
|
||||
|
||||
property real progress: 0.0
|
||||
property int progressFails: 0
|
||||
property int progressImported: 0
|
||||
property int progressSkipped: 0
|
||||
property string progressDescription: "nothing"
|
||||
property string progressInit: "init"
|
||||
property int total: 42
|
||||
@ -1011,6 +1013,8 @@ Window {
|
||||
property SequentialAnimation animateProgressBar : SequentialAnimation {
|
||||
id: apb
|
||||
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: "progressDescription"; to: go.progressInit; duration: 1; }
|
||||
PropertyAnimation{ duration: 2000/apb.speedup; }
|
||||
@ -1024,6 +1028,8 @@ Window {
|
||||
PropertyAnimation{ target: go; properties: "progress"; to: 0.01; duration: 1; }
|
||||
PropertyAnimation{ duration: 1000/apb.speedup; }
|
||||
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{ target: go; properties: "progress"; to: 0.3; duration: 1; }
|
||||
PropertyAnimation{ target: go; properties: "progressFails"; to: 1; duration: 1; }
|
||||
|
||||
@ -21,6 +21,7 @@ package qtcommon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
|
||||
@ -208,7 +208,7 @@ func (a *Accounts) Auth2FA(twoFacAuth string) int {
|
||||
if a.auth == nil || a.authClient == nil {
|
||||
err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient)
|
||||
} else {
|
||||
_, err = a.authClient.Auth2FA(twoFacAuth, a.auth)
|
||||
err = a.authClient.Auth2FA(twoFacAuth, a.auth)
|
||||
}
|
||||
|
||||
if a.showLoginError(err, "auth2FA") {
|
||||
|
||||
@ -72,6 +72,9 @@ func (e *ErrorListModel) data(index *core.QModelIndex, role int) *core.QVariant
|
||||
case MailSubject:
|
||||
return qtcommon.NewQVariantString(r.Subject)
|
||||
case MailDate:
|
||||
if r.Time.IsZero() {
|
||||
return qtcommon.NewQVariantString("Unavailable")
|
||||
}
|
||||
return qtcommon.NewQVariantString(r.Time.String())
|
||||
case MailFrom:
|
||||
return qtcommon.NewQVariantString(r.From)
|
||||
|
||||
@ -337,20 +337,25 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
|
||||
if progress.IsStopped() {
|
||||
break
|
||||
}
|
||||
failed, imported, _, _, total := progress.GetCounts()
|
||||
if total != 0 {
|
||||
f.Qml.SetTotal(int(total))
|
||||
}
|
||||
f.Qml.SetProgressFails(int(failed))
|
||||
counts := progress.GetCounts()
|
||||
f.Qml.SetTotal(int(counts.Total))
|
||||
f.Qml.SetProgressImported(int(counts.Imported))
|
||||
f.Qml.SetProgressSkipped(int(counts.Skipped))
|
||||
f.Qml.SetProgressFails(int(counts.Failed))
|
||||
f.Qml.SetProgressDescription(progress.PauseReason())
|
||||
if total > 0 {
|
||||
newProgress := float32(imported+failed) / float32(total)
|
||||
if counts.Total > 0 {
|
||||
newProgress := counts.Progress()
|
||||
if newProgress >= 0 && newProgress != f.Qml.Progress() {
|
||||
f.Qml.SetProgress(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 {
|
||||
f.Qml.SetProgressDescription(err.Error())
|
||||
@ -428,6 +433,10 @@ func (f *FrontendQt) resetSource() {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FrontendQt) openLicenseFile() {
|
||||
go open.Run(f.config.GetLicenseFilePath())
|
||||
}
|
||||
|
||||
// getLocalVersionInfo is identical to bridge.
|
||||
func (f *FrontendQt) getLocalVersionInfo() {
|
||||
defer f.Qml.ProcessFinished()
|
||||
|
||||
@ -73,15 +73,18 @@ func (f *FrontendQt) loadStructuresForImport() error {
|
||||
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")
|
||||
|
||||
f.Qml.SetProgressDescription("init") // TODO use const
|
||||
f.Qml.SetProgressImported(0)
|
||||
f.Qml.SetProgressSkipped(0)
|
||||
f.Qml.SetProgressFails(0)
|
||||
f.Qml.SetProgress(0.0)
|
||||
f.Qml.SetTotal(1)
|
||||
f.Qml.SetImportLogFileName("")
|
||||
|
||||
f.transfer.SetSkipEncryptedMessages(!importEncrypted)
|
||||
progress := f.transfer.Start()
|
||||
|
||||
f.Qml.SetImportLogFileName(progress.FileReport())
|
||||
|
||||
@ -43,6 +43,8 @@ type GoQMLInterface struct {
|
||||
_ string `property:lastError`
|
||||
_ float32 `property:progress`
|
||||
_ string `property:progressDescription`
|
||||
_ int `property:progressImported`
|
||||
_ int `property:progressSkipped`
|
||||
_ int `property:progressFails`
|
||||
_ int `property:total`
|
||||
_ string `property:importLogFileName`
|
||||
@ -71,6 +73,7 @@ type GoQMLInterface struct {
|
||||
_ func(okay bool) `signal:"importStructuresLoadFinished"`
|
||||
_ func() `signal:"openManual"`
|
||||
_ func(showMessage bool) `signal:"runCheckVersion"`
|
||||
_ func() `slot:"openLicenseFile"`
|
||||
_ func() `slot:"getLocalVersionInfo"`
|
||||
_ func() `slot:"loadImportReports"`
|
||||
|
||||
@ -93,7 +96,7 @@ type GoQMLInterface struct {
|
||||
_ func() string `slot:"leastUsedColor"`
|
||||
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
|
||||
_ 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(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.SetProgramTitle(f.programName)
|
||||
|
||||
s.ConnectOpenLicenseFile(f.openLicenseFile)
|
||||
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
|
||||
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
|
||||
s.ConnectGetBackendVersion(func() string {
|
||||
|
||||
@ -164,7 +164,7 @@ func (s *FrontendQt) auth2FA(twoFacAuth string) int {
|
||||
if s.auth == nil || s.authClient == nil {
|
||||
err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient)
|
||||
} else {
|
||||
_, err = s.authClient.Auth2FA(twoFacAuth, s.auth)
|
||||
err = s.authClient.Auth2FA(twoFacAuth, s.auth)
|
||||
}
|
||||
|
||||
if s.showLoginError(err, "auth2FA") {
|
||||
|
||||
@ -40,17 +40,17 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"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/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"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/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/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/therecipe/qt/core"
|
||||
"github.com/therecipe/qt/gui"
|
||||
@ -187,7 +187,6 @@ func (s *FrontendQt) watchEvents() {
|
||||
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent)
|
||||
newUserCh := s.getEventChannel(events.UserRefreshEvent)
|
||||
certIssue := s.getEventChannel(events.TLSCertIssue)
|
||||
imapCertIssue := s.getEventChannel(events.IMAPTLSBadCert)
|
||||
for {
|
||||
select {
|
||||
case errorDetails := <-errorCh:
|
||||
@ -227,8 +226,6 @@ func (s *FrontendQt) watchEvents() {
|
||||
s.Qml.LoadAccounts()
|
||||
case <-certIssue:
|
||||
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() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
localVersion := s.updates.GetLocalVersion()
|
||||
|
||||
@ -91,6 +91,7 @@ type GoQMLInterface struct {
|
||||
_ func() `slot:"errorSystray"`
|
||||
_ func() `slot:"normalSystray"`
|
||||
|
||||
_ func() `slot:"openLicenseFile"`
|
||||
_ func() `slot:"getLocalVersionInfo"`
|
||||
_ func(showMessage bool) `slot:"isNewVersionAvailable"`
|
||||
_ func() string `slot:"getBackendVersion"`
|
||||
@ -135,7 +136,6 @@ type GoQMLInterface struct {
|
||||
_ func(x, y float32) `slot:"saveOutgoingNoEncPopupCoord"`
|
||||
_ func(recipient string) `signal:"showNoActiveKeyForRecipient"`
|
||||
_ func() `signal:"showCertIssue"`
|
||||
_ func() `signal:"ShowIMAPCertTroubleshoot"`
|
||||
|
||||
_ func() `slot:"startUpdate"`
|
||||
_ func(hasError bool) `signal:"updateFinished"`
|
||||
@ -153,6 +153,7 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectClearCache(f.clearCache)
|
||||
s.ConnectClearKeychain(f.clearKeychain)
|
||||
|
||||
s.ConnectOpenLicenseFile(f.openLicenseFile)
|
||||
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
|
||||
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
|
||||
s.ConnectGetIMAPPort(f.getIMAPPort)
|
||||
|
||||
@ -23,13 +23,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
imapid "github.com/ProtonMail/go-imap-id"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/emersion/go-imap"
|
||||
goIMAPBackend "github.com/emersion/go-imap/backend"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type panicHandler interface {
|
||||
@ -45,12 +43,12 @@ type imapBackend struct {
|
||||
users map[string]*imapUser
|
||||
usersLocker sync.Locker
|
||||
|
||||
lastMailClient imapid.ID
|
||||
lastMailClientLocker sync.Locker
|
||||
|
||||
imapCache map[string]map[string]string
|
||||
imapCachePath string
|
||||
imapCacheLock *sync.RWMutex
|
||||
|
||||
updatesBlocking map[string]bool
|
||||
updatesBlockingLocker sync.Locker
|
||||
}
|
||||
|
||||
// NewIMAPBackend returns struct implementing go-imap/backend interface.
|
||||
@ -63,10 +61,6 @@ func NewIMAPBackend(
|
||||
bridgeWrap := newBridgeWrap(bridge)
|
||||
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()
|
||||
|
||||
return backend
|
||||
@ -87,11 +81,11 @@ func newIMAPBackend(
|
||||
users: map[string]*imapUser{},
|
||||
usersLocker: &sync.Mutex{},
|
||||
|
||||
lastMailClient: imapid.ID{imapid.FieldName: clientNone},
|
||||
lastMailClientLocker: &sync.Mutex{},
|
||||
|
||||
imapCachePath: cfg.GetIMAPCachePath(),
|
||||
imapCacheLock: &sync.RWMutex{},
|
||||
|
||||
updatesBlocking: map[string]bool{},
|
||||
updatesBlockingLocker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,7 +158,9 @@ func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMA
|
||||
|
||||
if err := imapUser.user.CheckBridgeLogin(password); err != nil {
|
||||
log.WithError(err).Error("Could not check bridge password")
|
||||
_ = imapUser.Logout()
|
||||
if err := imapUser.Logout(); err != nil {
|
||||
log.WithError(err).Warn("Could not logout user after unsuccessful login check")
|
||||
}
|
||||
// Apple Mail sometimes generates a lot of requests very quickly.
|
||||
// It's therefore good to have a timeout after a bad login so that we can slow
|
||||
// those requests down a little bit.
|
||||
@ -175,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
|
||||
// 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).
|
||||
imapUser.user.SetIMAPIdleUpdateChannel()
|
||||
if store := imapUser.user.GetStore(); store != nil {
|
||||
store.SetChangeNotifier(ib)
|
||||
}
|
||||
|
||||
return imapUser, nil
|
||||
}
|
||||
@ -192,23 +190,6 @@ func (ib *imapBackend) CreateMessageLimit() *uint32 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ib *imapBackend) setLastMailClient(id imapid.ID) {
|
||||
ib.lastMailClientLocker.Lock()
|
||||
defer ib.lastMailClientLocker.Unlock()
|
||||
|
||||
if name, ok := id[imapid.FieldName]; ok && ib.lastMailClient[imapid.FieldName] != name {
|
||||
ib.lastMailClient = imapid.ID{}
|
||||
for k, v := range id {
|
||||
ib.lastMailClient[k] = v
|
||||
}
|
||||
log.Warn("Mail Client ID changed to ", ib.lastMailClient)
|
||||
ib.bridge.SetCurrentClient(
|
||||
ib.lastMailClient[imapid.FieldName],
|
||||
ib.lastMailClient[imapid.FieldVersion],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// monitorDisconnectedUsers removes users when it receives a close connection event for them.
|
||||
func (ib *imapBackend) monitorDisconnectedUsers() {
|
||||
ch := make(chan string)
|
||||
@ -220,11 +201,3 @@ func (ib *imapBackend) monitorDisconnectedUsers() {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ func (ib *imapBackend) removeFromCache(userID, label, toRemove string) {
|
||||
|
||||
func (ib *imapBackend) getCacheList(userID, label string) (list string) {
|
||||
if err := ib.loadIMAPCache(); err != nil {
|
||||
log.Warn("Could not load cache: ", err)
|
||||
log.WithError(err).Warn("Could not load cache")
|
||||
}
|
||||
|
||||
ib.imapCacheLock.Lock()
|
||||
@ -97,7 +97,9 @@ func (ib *imapBackend) getCacheList(userID, label string) (list string) {
|
||||
|
||||
ib.imapCacheLock.Unlock()
|
||||
|
||||
_ = ib.saveIMAPCache()
|
||||
if err := ib.saveIMAPCache(); err != nil {
|
||||
log.WithError(err).Warn("Could not save cache")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
169
internal/imap/backend_updates.go
Normal file
169
internal/imap/backend_updates.go
Normal 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
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,6 @@ type bridgeUser interface {
|
||||
IsCombinedAddressMode() bool
|
||||
GetAddressID(address string) (string, error)
|
||||
GetPrimaryAddress() string
|
||||
SetIMAPIdleUpdateChannel()
|
||||
UpdateUser() error
|
||||
Logout() error
|
||||
CloseConnection(address string)
|
||||
@ -76,5 +75,9 @@ func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
|
||||
}
|
||||
|
||||
func (u *bridgeUserWrap) GetStore() storeUserProvider {
|
||||
return newStoreUserWrap(u.User.GetStore())
|
||||
store := u.User.GetStore()
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return newStoreUserWrap(store)
|
||||
}
|
||||
|
||||
39
internal/imap/cache/cache_test.go
vendored
39
internal/imap/cache/cache_test.go
vendored
@ -18,7 +18,6 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
@ -59,34 +58,34 @@ func TestClearOld(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClearBig(t *testing.T) {
|
||||
msg := []byte("Test message")
|
||||
r := require.New(t)
|
||||
wantMessage := []byte("Test message")
|
||||
|
||||
nSize := 3
|
||||
cacheSizeLimit = nSize*len(msg) + 1
|
||||
cacheTimeLimit = int64(nSize * nSize * 2) // be sure the message will survive
|
||||
wantCacheSize := 3
|
||||
nTestMessages := wantCacheSize * wantCacheSize
|
||||
cacheSizeLimit = wantCacheSize*len(wantMessage) + 1
|
||||
cacheTimeLimit = int64(1 << 20) // be sure the message will survive
|
||||
|
||||
// It should have more than nSize items.
|
||||
for i := 0; i < nSize*nSize; i++ {
|
||||
// It should never have more than nSize items.
|
||||
for i := 0; i < nTestMessages; i++ {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
SaveMail(fmt.Sprintf("%s%d", testUID, i), msg, bs)
|
||||
if len(mailCache) > nSize {
|
||||
t.Error("Number of items in cache should not be more than", nSize)
|
||||
}
|
||||
SaveMail(fmt.Sprintf("%s%d", testUID, i), wantMessage, bs)
|
||||
r.LessOrEqual(len(mailCache), wantCacheSize, "cache too big when %d", i)
|
||||
}
|
||||
|
||||
// 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)
|
||||
reader, _ := LoadMail(iUID)
|
||||
if i < nSize*(nSize-1) && reader.Len() != 0 {
|
||||
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)
|
||||
mail := mailCache[iUID]
|
||||
|
||||
if i >= nSize*(nSize-1) && !bytes.Equal(stored, msg) {
|
||||
t.Error("LoadMail returned wrong message:", stored, iUID)
|
||||
if i < (nTestMessages - wantCacheSize) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
internal/imap/id/extension.go
Normal file
90
internal/imap/id/extension.go
Normal file
@ -0,0 +1,90 @@
|
||||
// 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 id
|
||||
|
||||
import (
|
||||
imapid "github.com/ProtonMail/go-imap-id"
|
||||
imapserver "github.com/emersion/go-imap/server"
|
||||
)
|
||||
|
||||
type currentClientSetter interface {
|
||||
SetCurrentClient(name, version string)
|
||||
}
|
||||
|
||||
// Extension for IMAP server
|
||||
type extension struct {
|
||||
extID imapserver.ConnExtension
|
||||
setter currentClientSetter
|
||||
}
|
||||
|
||||
func (ext *extension) Capabilities(conn imapserver.Conn) []string {
|
||||
return ext.extID.Capabilities(conn)
|
||||
}
|
||||
|
||||
func (ext *extension) Command(name string) imapserver.HandlerFactory {
|
||||
newIDHandler := ext.extID.Command(name)
|
||||
if newIDHandler == nil {
|
||||
return nil
|
||||
}
|
||||
return func() imapserver.Handler {
|
||||
if hdlrID, ok := newIDHandler().(*imapid.Handler); ok {
|
||||
return &handler{
|
||||
hdlrID: hdlrID,
|
||||
setter: ext.setter,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ext *extension) NewConn(conn imapserver.Conn) imapserver.Conn {
|
||||
return ext.extID.NewConn(conn)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
hdlrID *imapid.Handler
|
||||
setter currentClientSetter
|
||||
}
|
||||
|
||||
func (hdlr *handler) Parse(fields []interface{}) error {
|
||||
return hdlr.hdlrID.Parse(fields)
|
||||
}
|
||||
|
||||
func (hdlr *handler) Handle(conn imapserver.Conn) error {
|
||||
err := hdlr.hdlrID.Handle(conn)
|
||||
if err == nil {
|
||||
id := hdlr.hdlrID.Command.ID
|
||||
hdlr.setter.SetCurrentClient(
|
||||
id[imapid.FieldName],
|
||||
id[imapid.FieldVersion],
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// NewExtension returns extension which is adding RFC2871 ID capability, with
|
||||
// direct interface to set information about email client to backend.
|
||||
func NewExtension(serverID imapid.ID, setter currentClientSetter) imapserver.Extension {
|
||||
if conExtID, ok := imapid.NewExtension(serverID).(imapserver.ConnExtension); ok {
|
||||
return &extension{
|
||||
extID: conExtID,
|
||||
setter: setter,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -22,12 +22,6 @@ import "github.com/sirupsen/logrus"
|
||||
const (
|
||||
fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP).
|
||||
fetchAttachmentsWorkers = 5 // In how many workers to fetch attachments (for one message).
|
||||
|
||||
clientAppleMail = "Mac OS X Mail" //nolint[deadcode]
|
||||
clientThunderbird = "Thunderbird" //nolint[deadcode]
|
||||
clientOutlookMac = "Microsoft Outlook for Mac" //nolint[deadcode]
|
||||
clientOutlookWin = "Microsoft Outlook" //nolint[deadcode]
|
||||
clientNone = ""
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@ -80,7 +80,10 @@ func (im *imapMailbox) Info() (*imap.MailboxInfo, error) {
|
||||
}
|
||||
|
||||
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() {
|
||||
case pmapi.SentLabel:
|
||||
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()
|
||||
status := imap.NewMailboxStatus(im.name, items)
|
||||
status.UidValidity = im.storeMailbox.UIDValidity()
|
||||
status.PermanentFlags = []string{
|
||||
status.Flags = []string{
|
||||
imap.SeenFlag, strings.ToUpper(imap.SeenFlag),
|
||||
imap.FlaggedFlag, strings.ToUpper(imap.FlaggedFlag),
|
||||
imap.DeletedFlag, strings.ToUpper(imap.DeletedFlag),
|
||||
@ -124,6 +127,7 @@ func (im *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, err
|
||||
message.ThunderbirdJunkFlag,
|
||||
message.ThunderbirdNonJunkFlag,
|
||||
}
|
||||
status.PermanentFlags = append([]string{}, status.Flags...)
|
||||
|
||||
dbTotal, dbUnread, dbUnreadSeqNum, err := im.storeMailbox.GetCounts()
|
||||
l.WithFields(logrus.Fields{
|
||||
@ -173,9 +177,11 @@ func (im *imapMailbox) Check() error {
|
||||
|
||||
// Expunge permanently removes all messages that have the \Deleted flag set
|
||||
// from the currently selected mailbox.
|
||||
// Our messages do not have \Deleted flag, nothing to do here.
|
||||
func (im *imapMailbox) Expunge() error {
|
||||
return nil
|
||||
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()
|
||||
}
|
||||
|
||||
func (im *imapMailbox) ListQuotas() ([]string, error) {
|
||||
|
||||
@ -24,7 +24,6 @@ import (
|
||||
"mime/multipart"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@ -37,11 +36,14 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
enmime "github.com/jhillyerd/enmime"
|
||||
"github.com/pkg/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 }
|
||||
|
||||
func (dnc *doNotCacheError) Error() string { return dnc.e.Error() }
|
||||
@ -142,18 +144,19 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
|
||||
references := m.Header.Get("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]
|
||||
// In case we are using a mail client which corrupts headers, try "References" too.
|
||||
re := regexp.MustCompile(pmapi.InternalReferenceFormat)
|
||||
match := re.FindStringSubmatch(lastReference)
|
||||
if len(match) > 0 {
|
||||
internalID = match[0]
|
||||
match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference)
|
||||
if len(match) == 2 {
|
||||
internalID = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid appending a message which is already on the server. Apply the new
|
||||
// label instead. This sometimes happens which Outlook (it uses APPEND instead of COPY).
|
||||
// Avoid appending a message which is already on the server. Apply the
|
||||
// new label instead. This always happens with Outlook (it uses APPEND
|
||||
// instead of COPY).
|
||||
if internalID != "" {
|
||||
// Check to see if this belongs to a different address in split mode or another ProtonMail account.
|
||||
msg, err := im.storeMailbox.GetMessage(internalID)
|
||||
@ -221,6 +224,9 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
|
||||
}
|
||||
case imap.FetchFlags:
|
||||
msg.Flags = message.GetFlags(m)
|
||||
if storeMessage.IsMarkedDeleted() {
|
||||
msg.Flags = append(msg.Flags, imap.DeletedFlag)
|
||||
}
|
||||
case imap.FetchInternalDate:
|
||||
msg.InternalDate = time.Unix(m.Time, 0)
|
||||
case imap.FetchRFC822Size:
|
||||
@ -238,26 +244,30 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
s := item
|
||||
|
||||
var section *imap.BodySectionName
|
||||
if section, err = imap.ParseBodySectionName(s); err != nil {
|
||||
err = nil // Ignore error
|
||||
break
|
||||
}
|
||||
|
||||
var literal imap.Literal
|
||||
if literal, err = im.getMessageBodySection(storeMessage, section); err != nil {
|
||||
if err = im.getLiteralForSection(item, msg, storeMessage); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg.Body[section] = literal
|
||||
}
|
||||
}
|
||||
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider) error {
|
||||
section, err := imap.ParseBodySectionName(itemSection)
|
||||
if err != nil { // Ignore error
|
||||
return nil
|
||||
}
|
||||
|
||||
var literal imap.Literal
|
||||
if literal, err = im.getMessageBodySection(storeMessage, section); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg.Body[section] = literal
|
||||
return nil
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (
|
||||
structure *message.BodyStructure,
|
||||
bodyReader *bytes.Reader, err error,
|
||||
@ -446,17 +456,6 @@ func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err erro
|
||||
return
|
||||
}
|
||||
|
||||
func (im *imapMailbox) writeAndParseMIMEBody(m *pmapi.Message) (mime *enmime.Envelope, err error) { //nolint[unused]
|
||||
b := &bytes.Buffer{}
|
||||
if err = im.writeMessageBody(b, m); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mime, err = enmime.ReadEnvelope(b)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (im *imapMailbox) writeAttachmentBody(w io.Writer, m *pmapi.Message, att *pmapi.Attachment) (err error) {
|
||||
// Retrieve encrypted attachment.
|
||||
r, err := im.user.client().GetAttachment(att.ID)
|
||||
@ -610,7 +609,7 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (
|
||||
}
|
||||
|
||||
tmpBuf := &bytes.Buffer{}
|
||||
mainHeader := message.GetHeader(m)
|
||||
mainHeader := buildHeader(m)
|
||||
if err = writeHeader(tmpBuf, mainHeader); err != nil {
|
||||
return
|
||||
}
|
||||
@ -708,3 +707,23 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -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.
|
||||
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)
|
||||
if err != nil || len(messageIDs) == 0 {
|
||||
return err
|
||||
@ -57,7 +60,11 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
|
||||
return im.addOrRemoveFlags(operation, messageIDs, flags)
|
||||
}
|
||||
|
||||
func (im *imapMailbox) setFlags(messageIDs, flags []string) error {
|
||||
// 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
|
||||
seen := false
|
||||
flagged := false
|
||||
deleted := false
|
||||
@ -77,29 +84,48 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error {
|
||||
}
|
||||
|
||||
if seen {
|
||||
_ = im.storeMailbox.MarkMessagesRead(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_ = im.storeMailbox.MarkMessagesUnread(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if flagged {
|
||||
_ = im.storeMailbox.MarkMessagesStarred(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if deleted {
|
||||
_ = im.storeMailbox.DeleteMessages(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := im.storeMailbox.MarkMessagesUndeleted(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
spamMailbox, err := im.storeAddress.GetMailbox("Spam")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Spam should not be taken into action here as Outlook is using FLAGS
|
||||
// without preserving junk flag. Probably it's because junk is not standard
|
||||
// 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 {
|
||||
_ = spamMailbox.LabelMessages(messageIDs)
|
||||
} else {
|
||||
_ = spamMailbox.UnlabelMessages(messageIDs)
|
||||
spamMailbox, err := im.storeAddress.GetMailbox("Spam")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := spamMailbox.LabelMessages(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -111,22 +137,36 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
|
||||
case imap.SeenFlag:
|
||||
switch operation {
|
||||
case imap.AddFlags:
|
||||
_ = im.storeMailbox.MarkMessagesRead(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
case imap.RemoveFlags:
|
||||
_ = im.storeMailbox.MarkMessagesUnread(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case imap.FlaggedFlag:
|
||||
switch operation {
|
||||
case imap.AddFlags:
|
||||
_ = im.storeMailbox.MarkMessagesStarred(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
case imap.RemoveFlags:
|
||||
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs)
|
||||
if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case imap.DeletedFlag:
|
||||
if operation == imap.RemoveFlags {
|
||||
break // Nothing to do, no message has the \Deleted flag.
|
||||
switch operation {
|
||||
case imap.AddFlags:
|
||||
if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
case imap.RemoveFlags:
|
||||
if err := im.storeMailbox.MarkMessagesUndeleted(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_ = im.storeMailbox.DeleteMessages(messageIDs)
|
||||
case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag:
|
||||
// Not supported.
|
||||
case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag:
|
||||
@ -140,9 +180,13 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
|
||||
// No label removal is necessary because Spam and Inbox are both exclusive labels so the backend
|
||||
// will automatically take care of label removal.
|
||||
case imap.AddFlags:
|
||||
_ = storeMailbox.LabelMessages(messageIDs)
|
||||
if err := storeMailbox.LabelMessages(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
case imap.RemoveFlags:
|
||||
_ = storeMailbox.UnlabelMessages(messageIDs)
|
||||
if err := storeMailbox.UnlabelMessages(messageIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,6 +230,20 @@ func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel
|
||||
return err
|
||||
}
|
||||
|
||||
deletedIDs := []string{}
|
||||
allDeletedIDs, err := im.storeMailbox.GetDeletedAPIIDs()
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Problem to get deleted API IDs")
|
||||
} else {
|
||||
for _, messageID := range messageIDs {
|
||||
for _, deletedID := range allDeletedIDs {
|
||||
if messageID == deletedID {
|
||||
deletedIDs = append(deletedIDs, deletedID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Label messages first to not lose them. If message is only in trash and we unlabel
|
||||
// it, it will be removed completely and we cannot label it back.
|
||||
if err := targetStoreMailbox.LabelMessages(messageIDs); err != nil {
|
||||
@ -197,6 +255,13 @@ func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve \Deleted flag at target location.
|
||||
if len(deletedIDs) > 0 {
|
||||
if err := targetStoreMailbox.MarkMessagesDeleted(deletedIDs); err != nil {
|
||||
log.WithError(err).Warn("Problem to preserve deleted flag for copied messages")
|
||||
}
|
||||
}
|
||||
|
||||
targetSeqSet := targetStoreMailbox.GetUIDList(messageIDs)
|
||||
return uidplus.CopyResponse(targetStoreMailbox.UIDValidity(), sourceSeqSet, targetSeqSet)
|
||||
}
|
||||
@ -321,6 +386,9 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
if !m.Has(pmapi.FlagOpened) {
|
||||
messageFlagsMap[imap.RecentFlag] = true
|
||||
}
|
||||
if storeMessage.IsMarkedDeleted() {
|
||||
messageFlagsMap[imap.DeletedFlag] = true
|
||||
}
|
||||
|
||||
flagMatch := true
|
||||
for _, flag := range criteria.WithFlags {
|
||||
@ -383,6 +451,12 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
|
||||
im.panicHandler.HandlePanic()
|
||||
}()
|
||||
|
||||
// EXPUNGE cannot be sent during listing and can come only from
|
||||
// the event loop, so we prevent any server side update to avoid
|
||||
// the problem.
|
||||
im.storeUser.PauseEventLoop(true)
|
||||
defer im.storeUser.PauseEventLoop(false)
|
||||
|
||||
var markAsReadIDs []string
|
||||
markAsReadMutex := &sync.Mutex{}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
imapid "github.com/ProtonMail/go-imap-id"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/id"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/emersion/go-imap"
|
||||
@ -57,37 +58,14 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
s.AllowInsecureAuth = true
|
||||
s.ErrorLog = newServerErrorLogger("server-imap")
|
||||
s.AutoLogout = 30 * time.Minute
|
||||
s.UpgradeError = imapBackend.upgradeError
|
||||
|
||||
serverID := imapid.ID{
|
||||
imapid.FieldName: "ProtonMail",
|
||||
imapid.FieldName: "ProtonMail Bridge",
|
||||
imapid.FieldVendor: "Proton Technologies AG",
|
||||
imapid.FieldSupportURL: "https://protonmail.com/support",
|
||||
}
|
||||
|
||||
s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
|
||||
conn.Server().ForEachConn(func(candidate imapserver.Conn) {
|
||||
if id, ok := candidate.(imapid.Conn); ok {
|
||||
if conn.Context() == candidate.Context() {
|
||||
// ID is not available right at the beginning of the connection.
|
||||
// Clients send ID quickly after AUTH. We need to wait for it.
|
||||
go func() {
|
||||
start := time.Now()
|
||||
for {
|
||||
if id.ID() != nil {
|
||||
imapBackend.setLastMailClient(id.ID())
|
||||
break
|
||||
}
|
||||
if time.Since(start) > 10*time.Second {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return sasl.NewLoginServer(func(address, password string) error {
|
||||
user, err := conn.Server().Backend.Login(nil, address, password)
|
||||
if err != nil {
|
||||
@ -105,7 +83,7 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
imapidle.NewExtension(),
|
||||
imapmove.NewExtension(),
|
||||
imapspecialuse.NewExtension(),
|
||||
imapid.NewExtension(serverID),
|
||||
id.NewExtension(serverID, imapBackend.bridge),
|
||||
imapquota.NewExtension(),
|
||||
imapappendlimit.NewExtension(),
|
||||
imapunselect.NewExtension(),
|
||||
@ -148,7 +126,9 @@ func (s *imapServer) ListenAndServe() {
|
||||
|
||||
// Stops the server.
|
||||
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() {
|
||||
@ -161,7 +141,9 @@ func (s *imapServer) monitorDisconnectedUsers() {
|
||||
disconnectUser := func(conn imapserver.Conn) {
|
||||
connUser := conn.Context().User
|
||||
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)
|
||||
|
||||
@ -41,6 +41,10 @@ type storeUserProvider interface {
|
||||
attachedPublicKey,
|
||||
attachedPublicKeyName string,
|
||||
parentID string) (*pmapi.Message, []*pmapi.Attachment, error)
|
||||
|
||||
PauseEventLoop(bool)
|
||||
|
||||
SetChangeNotifier(store.ChangeNotifier)
|
||||
}
|
||||
|
||||
type storeAddressProvider interface {
|
||||
@ -68,6 +72,7 @@ type storeMailboxProvider interface {
|
||||
GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error)
|
||||
GetLatestAPIID() (string, error)
|
||||
GetNextUID() (uint32, error)
|
||||
GetDeletedAPIIDs() ([]string, error)
|
||||
GetCounts() (dbTotal, dbUnread, dbUnreadSeqNum uint, err error)
|
||||
GetUIDList(apiIDs []string) *uidplus.OrderedSeq
|
||||
GetUIDByHeader(header *mail.Header) uint32
|
||||
@ -81,8 +86,10 @@ type storeMailboxProvider interface {
|
||||
MarkMessagesUnread(apiID []string) error
|
||||
MarkMessagesStarred(apiID []string) error
|
||||
MarkMessagesUnstarred(apiID []string) error
|
||||
MarkMessagesDeleted(apiID []string) error
|
||||
MarkMessagesUndeleted(apiID []string) error
|
||||
ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error
|
||||
DeleteMessages(apiID []string) error
|
||||
RemoveDeleted() error
|
||||
}
|
||||
|
||||
type storeMessageProvider interface {
|
||||
@ -90,6 +97,7 @@ type storeMessageProvider interface {
|
||||
UID() (uint32, error)
|
||||
SequenceNumber() (uint32, error)
|
||||
Message() *pmapi.Message
|
||||
IsMarkedDeleted() bool
|
||||
|
||||
SetSize(int64) error
|
||||
SetContentTypeAndHeader(string, mail.Header) error
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
package uidplus
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
@ -113,18 +114,43 @@ func (os *OrderedSeq) String() string {
|
||||
return out
|
||||
}
|
||||
|
||||
// UIDExpunge implements server.Handler but has no effect because Bridge is not
|
||||
// using EXPUNGE at all. The message is deleted right after it was flagged as
|
||||
// \Deleted Bridge should simply ignore this command with empty `OK` response.
|
||||
//
|
||||
// If not implemented it would cause harmless IMAP error.
|
||||
//
|
||||
// This overrides the standard EXPUNGE functionality.
|
||||
type UIDExpunge struct{}
|
||||
// UIDExpunge implements server.Handler but Bridge is not supporting
|
||||
// UID EXPUNGE with specific UIDs.
|
||||
type UIDExpunge struct {
|
||||
expunge *server.Expunge
|
||||
}
|
||||
|
||||
func (e *UIDExpunge) Parse(fields []interface{}) error { log.Traceln("parse", fields); return nil }
|
||||
func (e *UIDExpunge) Handle(conn server.Conn) error { log.Traceln("handle"); return nil }
|
||||
func (e *UIDExpunge) UidHandle(conn server.Conn) error { log.Traceln("uid handle"); return nil } //nolint[golint]
|
||||
func newUIDExpunge() *UIDExpunge {
|
||||
return &UIDExpunge{expunge: &server.Expunge{}}
|
||||
}
|
||||
|
||||
func (e *UIDExpunge) Parse(fields []interface{}) error {
|
||||
if len(fields) < 1 {
|
||||
return e.expunge.Parse(fields)
|
||||
}
|
||||
|
||||
// RFC4315#section-2.1
|
||||
// The UID EXPUNGE command permanently removes all messages that both
|
||||
// have the \Deleted flag set and have a UID that is included in the
|
||||
// specified sequence set from the currently selected mailbox. If a
|
||||
// message either does not have the \Deleted flag set or has a UID
|
||||
// that is not included in the specified sequence set, it is not
|
||||
// affected.
|
||||
//
|
||||
// Current implementation supports only deletion of all messages
|
||||
// marked as deleted. It will probably need mailbox interface change:
|
||||
// ExpungeUIDs(seqSet). Not sure how to combine with original
|
||||
// e.expunge.Handle().
|
||||
return errors.New("UID EXPUNGE with UIDs is not supported")
|
||||
}
|
||||
|
||||
func (e *UIDExpunge) Handle(conn server.Conn) error {
|
||||
return e.expunge.Handle(conn)
|
||||
}
|
||||
|
||||
func (e *UIDExpunge) UidHandle(conn server.Conn) error { //nolint[golint]
|
||||
return e.expunge.Handle(conn)
|
||||
}
|
||||
|
||||
type extension struct{}
|
||||
|
||||
@ -143,7 +169,7 @@ func (ext *extension) Capabilities(c server.Conn) []string {
|
||||
func (ext *extension) Command(name string) server.HandlerFactory {
|
||||
if name == "EXPUNGE" {
|
||||
return func() server.Handler {
|
||||
return &UIDExpunge{}
|
||||
return newUIDExpunge()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at Fri 04 Sep 2020 01:57:36 PM CEST. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Fri Nov 27 09:23:06 CET 2020. DO NOT EDIT.
|
||||
|
||||
package importexport
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-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-delve/delve;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameshoulahan/go-imap;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/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/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/psampaz/go-mod-outdated;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/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;"
|
||||
|
||||
@ -15,19 +15,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./release-notes.sh at 'Fri 04 Sep 2020 01:57:36 PM CEST'. DO NOT EDIT.
|
||||
// Code generated by ./release-notes.sh at 'Wed Dec 9 05:57:01 PM CET 2020'. DO NOT EDIT.
|
||||
|
||||
package importexport
|
||||
|
||||
const ReleaseNotes = `***Note: If you were using the Import-Export app before, you need to uninstall the older version and log in again to the new version***
|
||||
|
||||
• Complete code refactor in preparation of stable and open source release of the Import-Export app
|
||||
• Increased number of supported mail providers by changing the way the folder structures are handled (NIL hierarchy delimiter)
|
||||
• Improved handling for unstable internet and pause and resume behavior
|
||||
const ReleaseNotes = `• Allow an import of already encrypted messages (as cypher text)
|
||||
• Cosmetic GUI changes
|
||||
• Better error handling
|
||||
`
|
||||
|
||||
const ReleaseFixedBugs = `• Fixed rare cases where the application freezes when starting/stopping imports
|
||||
• Allowed current date to be included in the selected date range for both import and export
|
||||
• Improved manual update process
|
||||
• Limit space usage by on device application logs
|
||||
const ReleaseFixedBugs = `• Installation issues on linux
|
||||
`
|
||||
|
||||
@ -27,6 +27,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/confirmer"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
goSMTPBackend "github.com/emersion/go-smtp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -70,7 +71,7 @@ func newSMTPBackend(
|
||||
}
|
||||
|
||||
// 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.
|
||||
defer sb.panicHandler.HandlePanic()
|
||||
username = strings.ToLower(username)
|
||||
@ -97,7 +98,14 @@ func (sb *smtpBackend) Login(username, password string) (goSMTPBackend.User, err
|
||||
if user.IsCombinedAddressMode() {
|
||||
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 {
|
||||
|
||||
@ -45,7 +45,7 @@ type SendPreferences struct {
|
||||
// internal emails (including the so-called encrypted-to-outside emails,
|
||||
// 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.
|
||||
Scheme int
|
||||
Scheme pmapi.PackageFlag
|
||||
|
||||
// MIMEType is the MIME type to use for formatting the body of the email
|
||||
// (before encryption/after decryption). The standard possibilities are the
|
||||
@ -95,9 +95,8 @@ func (b *sendPreferencesBuilder) shouldEncrypt() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withSign() {
|
||||
v := true
|
||||
b.sign = &v
|
||||
func (b *sendPreferencesBuilder) withSign(sign bool) {
|
||||
b.sign = &sign
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withSignDefault() {
|
||||
@ -193,7 +192,11 @@ func (b *sendPreferencesBuilder) build() (p SendPreferences) {
|
||||
}
|
||||
|
||||
case b.shouldSign() && !b.shouldEncrypt():
|
||||
p.Scheme = pmapi.ClearMIMEPackage
|
||||
if b.getScheme() == pgpInline {
|
||||
p.Scheme = pmapi.ClearPackage
|
||||
} else {
|
||||
p.Scheme = pmapi.ClearMIMEPackage
|
||||
}
|
||||
|
||||
default:
|
||||
p.Scheme = pmapi.ClearPackage
|
||||
@ -258,7 +261,7 @@ func (b *sendPreferencesBuilder) setInternalPGPSettings(
|
||||
|
||||
// We always encrypt and sign internal mail.
|
||||
b.withEncrypt(true)
|
||||
b.withSign()
|
||||
b.withSign(true)
|
||||
|
||||
// We use a custom scheme for internal messages.
|
||||
b.withScheme(pmInternal)
|
||||
@ -369,7 +372,7 @@ func (b *sendPreferencesBuilder) setExternalPGPSettingsWithWKDKeys(
|
||||
|
||||
// We always encrypt and sign external mail if WKD keys are present.
|
||||
b.withEncrypt(true)
|
||||
b.withSign()
|
||||
b.withSign(true)
|
||||
|
||||
// If the contact has a specific Scheme preference, we set it (otherwise we
|
||||
// leave it unset to allow it to be filled in with the default value later).
|
||||
@ -402,9 +405,13 @@ func (b *sendPreferencesBuilder) setExternalPGPSettingsWithoutWKDKeys(
|
||||
) (err error) {
|
||||
b.withEncrypt(vCardData.Encrypt)
|
||||
|
||||
if vCardData.SignIsSet {
|
||||
b.withSign(vCardData.Sign)
|
||||
}
|
||||
|
||||
// Sign must be enabled whenever encrypt is.
|
||||
if vCardData.Sign || vCardData.Encrypt {
|
||||
b.withSign()
|
||||
if vCardData.Encrypt {
|
||||
b.withSign(true)
|
||||
}
|
||||
|
||||
// If the contact has a specific Scheme preference, we set it (otherwise we
|
||||
@ -475,7 +482,7 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
|
||||
}
|
||||
|
||||
if b.shouldEncrypt() {
|
||||
b.withSign()
|
||||
b.withSign(true)
|
||||
}
|
||||
|
||||
// If undefined, default to the user mail setting "Default PGP scheme".
|
||||
@ -495,23 +502,11 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
|
||||
if b.shouldSign() && b.getScheme() == pgpInline {
|
||||
b.withMIMEType("text/plain")
|
||||
} else {
|
||||
switch mailSettings.ComposerMode {
|
||||
case pmapi.ComposerModeNormal:
|
||||
b.withMIMETypeDefault("text/html")
|
||||
case pmapi.ComposerModePlain:
|
||||
b.withMIMETypeDefault("text/plain")
|
||||
}
|
||||
b.withMIMETypeDefault(mailSettings.DraftMIMEType)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// type is determined by the PGP scheme (also determined above): we should
|
||||
// use 'text/plain' for a PGP/Inline scheme, and 'multipart/mixed' otherwise.
|
||||
|
||||
@ -41,7 +41,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
|
||||
wantEncrypt bool
|
||||
wantSign bool
|
||||
wantScheme int
|
||||
wantScheme pmapi.PackageFlag
|
||||
wantMIMEType string
|
||||
wantPublicKey string
|
||||
}{
|
||||
@ -51,7 +51,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: true,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -66,7 +66,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: true,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -81,7 +81,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: true,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -97,7 +97,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: true,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -112,7 +112,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -127,7 +127,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -142,7 +142,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -157,7 +157,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Scheme: pgpInline},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -172,7 +172,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Scheme: pgpMIME},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -187,7 +187,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -203,7 +203,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -218,7 +218,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: false,
|
||||
@ -232,7 +232,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: false,
|
||||
@ -243,10 +243,38 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
{
|
||||
name: "external with sign enabled",
|
||||
|
||||
contactMeta: &ContactMetadata{Sign: true},
|
||||
contactMeta: &ContactMetadata{Sign: true, SignIsSet: true},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.ClearMIMEPackage,
|
||||
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,
|
||||
@ -260,7 +288,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: false,
|
||||
@ -272,10 +300,10 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
{
|
||||
name: "external with pinned contact public key, encrypted and signed",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true},
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -287,10 +315,10 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
{
|
||||
name: "external with pinned contact public key, encrypted and signed using contact-specific pgp-inline",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, Scheme: pgpInline},
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, Scheme: pgpInline, SignIsSet: true},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
@ -302,10 +330,10 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
{
|
||||
name: "external with pinned contact public key, encrypted and signed using global pgp-inline",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true},
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal},
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
|
||||
@ -20,6 +20,7 @@ package smtp
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -48,6 +49,15 @@ func newSendRecorder() *sendRecorder {
|
||||
}
|
||||
|
||||
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.Write([]byte(message.AddressID + message.Subject))
|
||||
if message.Sender != nil {
|
||||
@ -101,6 +111,10 @@ func (q *sendRecorder) isSendingOrSent(client messageGetter, hash string) (isSen
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
if hash == "" {
|
||||
return false, false
|
||||
}
|
||||
|
||||
q.deleteExpiredKeys()
|
||||
value, ok := q.hashes[hash]
|
||||
if !ok {
|
||||
|
||||
@ -349,6 +349,32 @@ func TestSendRecorder_getMessageHash(t *testing.T) {
|
||||
},
|
||||
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 {
|
||||
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.MessageTypeSent}, 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 {
|
||||
tc := tc // bind
|
||||
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}
|
||||
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.wantWasSent, wasSent, "wasSent does not match")
|
||||
})
|
||||
|
||||
@ -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 {
|
||||
return sasl.NewLoginServer(func(address, password string) error {
|
||||
user, err := conn.Server().Backend.Login(address, password)
|
||||
user, err := conn.Server().Backend.Login(nil, address, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.SetUser(user)
|
||||
conn.SetSession(user)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
@ -85,14 +85,16 @@ func (s *smtpServer) ListenAndServe() {
|
||||
l.Error("SMTP failed: ", err)
|
||||
return
|
||||
}
|
||||
defer s.server.Close()
|
||||
defer s.server.Close() //nolint[errcheck]
|
||||
|
||||
l.Info("SMTP server stopped")
|
||||
}
|
||||
|
||||
// Stops the server.
|
||||
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() {
|
||||
@ -102,9 +104,11 @@ func (s *smtpServer) monitorDisconnectedUsers() {
|
||||
for address := range ch {
|
||||
log.Info("Disconnecting all open SMTP connections for ", address)
|
||||
disconnectUser := func(conn *goSMTP.Conn) {
|
||||
connUser := conn.User()
|
||||
connUser := conn.Session()
|
||||
if connUser != nil {
|
||||
_ = conn.Close()
|
||||
if err := conn.Close(); err != nil {
|
||||
log.WithError(err).Error("Failed to close the connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
s.server.ForEachConn(disconnectUser)
|
||||
|
||||
@ -21,10 +21,10 @@ package smtp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -44,7 +44,11 @@ type smtpUser struct {
|
||||
backend *smtpBackend
|
||||
user bridgeUser
|
||||
storeUser storeUserProvider
|
||||
username string
|
||||
addressID string
|
||||
|
||||
from string
|
||||
to []string
|
||||
}
|
||||
|
||||
// newSMTPUser returns struct implementing go-smtp/session interface.
|
||||
@ -53,8 +57,9 @@ func newSMTPUser(
|
||||
eventListener listener.Listener,
|
||||
smtpBackend *smtpBackend,
|
||||
user bridgeUser,
|
||||
username string,
|
||||
addressID string,
|
||||
) (goSMTPBackend.User, error) {
|
||||
) (goSMTPBackend.Session, error) {
|
||||
storeUser := user.GetStore()
|
||||
if storeUser == nil {
|
||||
return nil, errors.New("user database is not initialized")
|
||||
@ -66,6 +71,7 @@ func newSMTPUser(
|
||||
backend: smtpBackend,
|
||||
user: user,
|
||||
storeUser: storeUser,
|
||||
username: username,
|
||||
addressID: addressID,
|
||||
}, nil
|
||||
}
|
||||
@ -145,6 +151,55 @@ func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey,
|
||||
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.
|
||||
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.
|
||||
@ -179,14 +234,15 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
return err
|
||||
}
|
||||
|
||||
attachedPublicKeyName = "publickey - " + kr.GetIdentities()[0].Name
|
||||
attachedPublicKeyName = fmt.Sprintf("publickey - %v - %v", kr.GetIdentities()[0].Name, firstKey.GetFingerprint()[:8])
|
||||
}
|
||||
|
||||
message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to parse message")
|
||||
return
|
||||
}
|
||||
clearBody := message.Body
|
||||
richBody := message.Body
|
||||
|
||||
externalID := message.Header.Get("Message-Id")
|
||||
externalID = strings.Trim(externalID, "<>")
|
||||
@ -255,7 +311,6 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
atts = append(atts, message.Attachments...)
|
||||
// Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys.
|
||||
attkeys := make(map[string]*crypto.SessionKey)
|
||||
attkeysEncoded := make(map[string]pmapi.AlgoKey)
|
||||
|
||||
for _, att := range atts {
|
||||
var keyPackets []byte
|
||||
@ -265,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 {
|
||||
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
|
||||
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
|
||||
|
||||
req := pmapi.NewSendMessageReq(kr, mimeBody, plainBody, richBody, attkeys)
|
||||
containsUnencryptedRecipients := false
|
||||
|
||||
for _, email := range to {
|
||||
@ -290,65 +331,22 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
}
|
||||
|
||||
sendPreferences, err := su.getSendPreferences(email, message.MIMEType, mailSettings)
|
||||
if !sendPreferences.Encrypt {
|
||||
containsUnencryptedRecipients = true
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var signature int
|
||||
var signature pmapi.SignatureFlag
|
||||
if sendPreferences.Sign {
|
||||
signature = pmapi.YesSignature
|
||||
signature = pmapi.SignatureDetached
|
||||
} else {
|
||||
signature = pmapi.NoSignature
|
||||
signature = pmapi.SignatureNone
|
||||
}
|
||||
if sendPreferences.Scheme == pmapi.PGPMIMEPackage || sendPreferences.Scheme == pmapi.ClearMIMEPackage {
|
||||
if mimeKey == nil {
|
||||
if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
if err := req.AddRecipient(email, sendPreferences.Scheme, sendPreferences.PublicKey, signature, sendPreferences.MIMEType, sendPreferences.Encrypt); err != nil {
|
||||
return errors.Wrap(err, "failed to add recipient")
|
||||
}
|
||||
}
|
||||
|
||||
@ -359,36 +357,14 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
return errors.New("error decoding subject message " + message.Header.Get("Subject"))
|
||||
}
|
||||
if !su.continueSendingUnencryptedMail(subject) {
|
||||
_ = su.client().DeleteMessages([]string{message.ID})
|
||||
if err := su.client().DeleteMessages([]string{message.ID}); err != nil {
|
||||
log.WithError(err).Warn("Failed to delete canceled messages")
|
||||
}
|
||||
return errors.New("sending was canceled by user")
|
||||
}
|
||||
}
|
||||
|
||||
req := &pmapi.SendMessageReq{}
|
||||
|
||||
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)
|
||||
}
|
||||
req.PreparePackages()
|
||||
|
||||
return su.storeUser.SendMessage(message.ID, req)
|
||||
}
|
||||
@ -401,9 +377,9 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
|
||||
if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) {
|
||||
newReferences = append(newReferences, reference)
|
||||
} else { // internalid is the parentID.
|
||||
idMatch := regexp.MustCompile(pmapi.InternalReferenceFormat).FindStringSubmatch(reference)
|
||||
if len(idMatch) > 0 {
|
||||
lastID := strings.TrimSuffix(strings.Trim(idMatch[0], "<>"), "@protonmail.internalid")
|
||||
idMatch := pmapi.RxInternalReferenceFormat.FindStringSubmatch(reference)
|
||||
if len(idMatch) == 2 {
|
||||
lastID := idMatch[1]
|
||||
filter := &pmapi.MessagesFilter{ID: []string{lastID}}
|
||||
if su.addressID != "" {
|
||||
filter.AddressID = su.addressID
|
||||
|
||||
@ -18,11 +18,7 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"regexp"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals // Used like a constant
|
||||
@ -35,85 +31,3 @@ var mailFormat = regexp.MustCompile(`.+@.+\..+`)
|
||||
func looksLikeEmail(e string) bool {
|
||||
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
|
||||
}
|
||||
|
||||
@ -27,13 +27,13 @@ import (
|
||||
)
|
||||
|
||||
type ContactMetadata struct {
|
||||
Email string
|
||||
Keys []string
|
||||
Scheme string
|
||||
Sign bool
|
||||
SignMissing bool
|
||||
Encrypt bool
|
||||
MIMEType string
|
||||
Email string
|
||||
Keys []string
|
||||
Scheme string
|
||||
Sign bool
|
||||
SignIsSet bool
|
||||
Encrypt bool
|
||||
MIMEType string
|
||||
}
|
||||
|
||||
const (
|
||||
@ -72,22 +72,22 @@ func GetContactMetadataFromVCards(cards []pmapi.Card, email string) (contactMeta
|
||||
// Warn: ParseBool treats 1, T, True, true as true and 0, F, Fale, false as false.
|
||||
// However PMEL declares 'true' is true, 'false' is false. every other string is true
|
||||
encrypt, _ := strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMEncrypt, group))
|
||||
var sign, signMissing bool
|
||||
var sign, signIsSet bool
|
||||
if len(parsedCard[FieldPMSign]) == 0 {
|
||||
signMissing = true
|
||||
signIsSet = false
|
||||
} else {
|
||||
sign, _ = strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMSign, group))
|
||||
signMissing = false
|
||||
signIsSet = true
|
||||
}
|
||||
mimeType := parsedCard.GetValueByGroup(FieldPMMIMEType, group)
|
||||
return &ContactMetadata{
|
||||
Email: email,
|
||||
Keys: keys,
|
||||
Scheme: scheme,
|
||||
Sign: sign,
|
||||
SignMissing: signMissing,
|
||||
Encrypt: encrypt,
|
||||
MIMEType: mimeType,
|
||||
Email: email,
|
||||
Keys: keys,
|
||||
Scheme: scheme,
|
||||
Sign: sign,
|
||||
SignIsSet: signIsSet,
|
||||
Encrypt: encrypt,
|
||||
MIMEType: mimeType,
|
||||
}, nil
|
||||
}
|
||||
return &ContactMetadata{}, nil
|
||||
|
||||
@ -69,7 +69,7 @@ func (storeAddress *Address) init(foldersAndLabels []*pmapi.Label) (err error) {
|
||||
prefix := getLabelPrefix(label)
|
||||
|
||||
var mailbox *Mailbox
|
||||
if mailbox, err = txNewMailbox(tx, storeAddress, label.ID, prefix, label.Name, label.Color); err != nil {
|
||||
if mailbox, err = txNewMailbox(tx, storeAddress, label.ID, prefix, label.Path, label.Color); err != nil {
|
||||
storeAddress.log.
|
||||
WithError(err).
|
||||
WithField("labelID", label.ID).
|
||||
|
||||
@ -73,14 +73,14 @@ func (storeAddress *Address) createOrUpdateMailboxEvent(label *pmapi.Label) erro
|
||||
prefix := getLabelPrefix(label)
|
||||
mailbox, ok := storeAddress.mailboxes[label.ID]
|
||||
if !ok {
|
||||
mailbox, err := newMailbox(storeAddress, label.ID, prefix, label.Name, label.Color)
|
||||
mailbox, err := newMailbox(storeAddress, label.ID, prefix, label.Path, label.Color)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storeAddress.mailboxes[label.ID] = mailbox
|
||||
mailbox.store.imapMailboxCreated(storeAddress.address, mailbox.labelName)
|
||||
mailbox.store.notifyMailboxCreated(storeAddress.address, mailbox.labelName)
|
||||
} else {
|
||||
mailbox.labelName = prefix + label.Name
|
||||
mailbox.labelName = prefix + label.Path
|
||||
mailbox.color = label.Color
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -45,7 +45,9 @@ func (c *Cache) getEventID(userID string) string {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
_ = c.loadCache()
|
||||
if err := c.loadCache(); err != nil {
|
||||
log.WithError(err).Warn("Problem to load store cache")
|
||||
}
|
||||
|
||||
if c.cache == nil {
|
||||
c.cache = map[string]map[string]string{}
|
||||
|
||||
@ -18,106 +18,56 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"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
|
||||
// on which the imap backend listens for imap updates.
|
||||
func (store *Store) SetIMAPUpdateChannel(updates chan imapBackend.Update) {
|
||||
store.log.Debug("Listening for IMAP updates")
|
||||
|
||||
if store.imapUpdates = updates; store.imapUpdates == nil {
|
||||
store.log.Error("The IMAP Updates channel is nil")
|
||||
}
|
||||
type ChangeNotifier interface {
|
||||
Notice(address, notice string)
|
||||
UpdateMessage(
|
||||
address, mailboxName string,
|
||||
uid, sequenceNumber uint32,
|
||||
msg *pmapi.Message, hasDeletedFlag bool)
|
||||
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) {
|
||||
update := new(imapBackend.StatusUpdate)
|
||||
update.Update = imapBackend.NewUpdate(address, "")
|
||||
update.StatusResp = &imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
Code: imap.CodeAlert,
|
||||
Info: notice,
|
||||
}
|
||||
store.imapSendUpdate(update)
|
||||
// SetChangeNotifier sets notifier to be called once mailbox or message changes.
|
||||
func (store *Store) SetChangeNotifier(notifier ChangeNotifier) {
|
||||
store.notifier = notifier
|
||||
}
|
||||
|
||||
func (store *Store) imapUpdateMessage(address, mailboxName string, uid, sequenceNumber uint32, msg *pmapi.Message) {
|
||||
store.log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
"seqNum": sequenceNumber,
|
||||
"uid": uid,
|
||||
"flags": message.GetFlags(msg),
|
||||
}).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)
|
||||
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")
|
||||
func (store *Store) notifyNotice(address, notice string) {
|
||||
if store.notifier == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
store.log.Error("Could not send IMAP update (timeout)")
|
||||
return
|
||||
case store.imapUpdates <- update:
|
||||
}
|
||||
store.notifier.Notice(address, notice)
|
||||
}
|
||||
|
||||
func (store *Store) notifyUpdateMessage(address, mailboxName string, uid, sequenceNumber uint32, msg *pmapi.Message, hasDeletedFlag bool) {
|
||||
if store.notifier == nil {
|
||||
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))
|
||||
}
|
||||
|
||||
@ -21,52 +21,43 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
imapBackend "github.com/emersion/go-imap/backend"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateOrUpdateMessageIMAPUpdates(t *testing.T) {
|
||||
func TestNotifyChangeCreateOrUpdateMessage(t *testing.T) {
|
||||
m, clear := initMocks(t)
|
||||
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.store.SetIMAPUpdateChannel(updates)
|
||||
|
||||
go checkIMAPUpdates(t, updates, []func(interface{}) bool{
|
||||
checkMessageUpdate(addr1, "All Mail", 1, 1),
|
||||
checkMessageUpdate(addr1, "All Mail", 2, 2),
|
||||
})
|
||||
m.store.SetChangeNotifier(m.changeNotifier)
|
||||
|
||||
insertMessage(t, m, "msg1", "Test message 1", 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)
|
||||
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.store.SetIMAPUpdateChannel(updates)
|
||||
|
||||
go checkIMAPUpdates(t, updates, []func(interface{}) bool{
|
||||
checkMessageUpdate(addr1, "All Mail", 1, 1),
|
||||
checkMessageUpdate(addr1, "All Mail", 2, 2),
|
||||
})
|
||||
m.store.SetChangeNotifier(m.changeNotifier)
|
||||
|
||||
msg1 := getTestMessage("msg1", "Test message 1", 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}))
|
||||
|
||||
close(updates)
|
||||
}
|
||||
|
||||
func TestDeleteMessageIMAPUpdate(t *testing.T) {
|
||||
func TestNotifyChangeDeleteMessage(t *testing.T) {
|
||||
m, clear := initMocks(t)
|
||||
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, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel})
|
||||
|
||||
updates := make(chan imapBackend.Update)
|
||||
m.store.SetIMAPUpdateChannel(updates)
|
||||
go checkIMAPUpdates(t, updates, []func(interface{}) bool{
|
||||
checkMessageDelete(addr1, "All Mail", 2),
|
||||
checkMessageDelete(addr1, "All Mail", 1),
|
||||
})
|
||||
m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(2))
|
||||
m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(1))
|
||||
|
||||
m.store.SetChangeNotifier(m.changeNotifier)
|
||||
require.Nil(t, m.store.deleteMessageEvent("msg2"))
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,8 @@ type eventLoop struct {
|
||||
pollCh chan chan struct{}
|
||||
stopCh chan struct{}
|
||||
notifyStopCh chan struct{}
|
||||
isRunning bool
|
||||
isRunning bool // The whole event loop is running.
|
||||
isTickerPaused bool // The periodic loop is paused (but the event loop itself is still running).
|
||||
hasInternet bool
|
||||
|
||||
pollCounter int
|
||||
@ -59,6 +60,7 @@ func newEventLoop(cache *Cache, store *Store, user BridgeUser, events listener.L
|
||||
currentEventID: cache.getEventID(user.ID()),
|
||||
pollCh: make(chan chan struct{}),
|
||||
isRunning: false,
|
||||
isTickerPaused: false,
|
||||
|
||||
log: eventLog,
|
||||
|
||||
@ -68,10 +70,6 @@ func newEventLoop(cache *Cache, store *Store, user BridgeUser, events listener.L
|
||||
}
|
||||
}
|
||||
|
||||
func (loop *eventLoop) IsRunning() bool {
|
||||
return loop.isRunning
|
||||
}
|
||||
|
||||
func (loop *eventLoop) client() pmapi.Client {
|
||||
return loop.store.client()
|
||||
}
|
||||
@ -156,6 +154,10 @@ func (loop *eventLoop) loop() {
|
||||
close(loop.notifyStopCh)
|
||||
return
|
||||
case <-t.C:
|
||||
if loop.isTickerPaused {
|
||||
loop.log.Trace("Event loop paused, skipping")
|
||||
continue
|
||||
}
|
||||
// Randomise periodic calls within range pollInterval ± pollSpread to reduces potential load spikes on API.
|
||||
time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond)
|
||||
case eventProcessedCh = <-loop.pollCh:
|
||||
@ -569,7 +571,7 @@ func (loop *eventLoop) processNotices(l *logrus.Entry, notices []string) {
|
||||
for _, notice := range notices {
|
||||
l.Infof("Notice: %q", notice)
|
||||
for _, address := range loop.user.GetStoreAddresses() {
|
||||
loop.store.imapNotice(address, notice)
|
||||
loop.store.notifyNotice(address, notice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,10 +53,10 @@ func TestEventLoopProcessMoreEvents(t *testing.T) {
|
||||
}, nil),
|
||||
)
|
||||
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.
|
||||
go m.store.eventLoop.start()
|
||||
// Event loop runs in goroutine started during store creation (newStoreNoEvents).
|
||||
// Force to run the next event.
|
||||
m.store.eventLoop.pollNow()
|
||||
|
||||
// More events are processed right away.
|
||||
require.Eventually(t, func() bool {
|
||||
@ -77,35 +77,42 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) {
|
||||
subject := "old subject"
|
||||
newSubject := "new subject"
|
||||
|
||||
// First sync will add message with old subject to database.
|
||||
m.client.EXPECT().GetMessage("msg1").Return(&pmapi.Message{
|
||||
m.newStoreNoEvents(true, &pmapi.Message{
|
||||
ID: "msg1",
|
||||
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.
|
||||
go m.store.eventLoop.start()
|
||||
// Event loop runs in goroutine started during store creation (newStoreNoEvents).
|
||||
// Force to run the next event.
|
||||
m.store.eventLoop.pollNow()
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
msg, err := m.store.getMessageFromDB("msg1")
|
||||
return err == nil && msg.Subject == newSubject
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
select {
|
||||
case <-eventReceived:
|
||||
case <-time.After(5 * time.Second):
|
||||
require.Fail(t, "latestEventID was not processed")
|
||||
}
|
||||
|
||||
msg, err := m.store.getMessageFromDB("msg1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newSubject, msg.Subject)
|
||||
}
|
||||
|
||||
func TestEventLoopUpdateMessage(t *testing.T) {
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -38,10 +39,12 @@ type Mailbox struct {
|
||||
color string
|
||||
|
||||
log *logrus.Entry
|
||||
|
||||
isDeleting atomic.Value
|
||||
}
|
||||
|
||||
func newMailbox(storeAddress *Address, labelID, labelPrefix, labelName, color string) (mb *Mailbox, err error) {
|
||||
_ = storeAddress.store.db.Update(func(tx *bolt.Tx) error {
|
||||
err = storeAddress.store.db.Update(func(tx *bolt.Tx) error {
|
||||
mb, err = txNewMailbox(tx, storeAddress, labelID, labelPrefix, labelName, color)
|
||||
return err
|
||||
})
|
||||
@ -59,6 +62,7 @@ func txNewMailbox(tx *bolt.Tx, storeAddress *Address, labelID, labelPrefix, labe
|
||||
color: color,
|
||||
log: l,
|
||||
}
|
||||
mb.isDeleting.Store(false)
|
||||
|
||||
err := initMailboxBucket(tx, mb.getBucketName())
|
||||
if err != nil {
|
||||
@ -142,6 +146,9 @@ func initMailboxBucket(tx *bolt.Tx, bucketName []byte) error {
|
||||
if _, err := bucket.CreateBucketIfNotExists(apiIDsBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bucket.CreateBucketIfNotExists(deletedIDsBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -212,6 +219,7 @@ func (storeMailbox *Mailbox) Rename(newName string) error {
|
||||
// Deletion has to be propagated to all the same mailboxes in all addresses.
|
||||
// The propagation is processed by the event loop.
|
||||
func (storeMailbox *Mailbox) Delete() error {
|
||||
storeMailbox.isDeleting.Store(true)
|
||||
return storeMailbox.storeAddress.deleteMailbox(storeMailbox.labelID)
|
||||
}
|
||||
|
||||
@ -223,6 +231,14 @@ func (storeMailbox *Mailbox) GetDelimiter() string {
|
||||
// deleteMailboxEvent deletes the mailbox bucket.
|
||||
// This is called from the event loop.
|
||||
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 tx.Bucket(mailboxesBucket).DeleteBucket(storeMailbox.getBucketName())
|
||||
})
|
||||
@ -238,6 +254,11 @@ func (storeMailbox *Mailbox) txGetAPIIDsBucket(tx *bolt.Tx) *bolt.Bucket {
|
||||
return storeMailbox.txGetBucket(tx).Bucket(apiIDsBucket)
|
||||
}
|
||||
|
||||
// txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted
|
||||
func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket {
|
||||
return storeMailbox.txGetBucket(tx).Bucket(deletedIDsBucket)
|
||||
}
|
||||
|
||||
// txGetBucket returns the bucket of mailbox containing mapping buckets.
|
||||
func (storeMailbox *Mailbox) txGetBucket(tx *bolt.Tx) *bolt.Bucket {
|
||||
return tx.Bucket(mailboxesBucket).Bucket(storeMailbox.getBucketName())
|
||||
|
||||
@ -125,6 +125,7 @@ func (mc *mailboxCounts) getPMLabel() *pmapi.Label {
|
||||
return &pmapi.Label{
|
||||
ID: mc.LabelID,
|
||||
Name: mc.LabelName,
|
||||
Path: mc.LabelName,
|
||||
Color: mc.Color,
|
||||
Order: mc.Order,
|
||||
Type: pmapi.LabelTypeMailbox,
|
||||
@ -158,7 +159,7 @@ func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) er
|
||||
}
|
||||
|
||||
// Update mailbox info, but dont change on-API-counts.
|
||||
mailbox.LabelName = label.Name
|
||||
mailbox.LabelName = label.Path
|
||||
mailbox.Color = label.Color
|
||||
mailbox.Order = label.Order
|
||||
mailbox.IsFolder = label.Exclusive == 1
|
||||
|
||||
@ -129,7 +129,11 @@ func (storeMailbox *Mailbox) getUID(apiID string) (uid uint32, err error) {
|
||||
}
|
||||
|
||||
func (storeMailbox *Mailbox) txGetUID(tx *bolt.Tx, apiID string) (uint32, error) {
|
||||
b := storeMailbox.txGetAPIIDsBucket(tx)
|
||||
return storeMailbox.txGetUIDFromBucket(storeMailbox.txGetAPIIDsBucket(tx), apiID)
|
||||
}
|
||||
|
||||
// txGetUIDFromBucket expects pointer to API bucket.
|
||||
func (storeMailbox *Mailbox) txGetUIDFromBucket(b *bolt.Bucket, apiID string) (uint32, error) {
|
||||
v := b.Get([]byte(apiID))
|
||||
if v == nil {
|
||||
return 0, ErrNoSuchAPIID
|
||||
@ -137,6 +141,19 @@ func (storeMailbox *Mailbox) txGetUID(tx *bolt.Tx, apiID string) (uint32, error)
|
||||
return btoi(v), nil
|
||||
}
|
||||
|
||||
// GetDeletedAPIIDs returns API IDs in this mailbox for message ID.
|
||||
func (storeMailbox *Mailbox) GetDeletedAPIIDs() (apiIDs []string, err error) {
|
||||
err = storeMailbox.db().Update(func(tx *bolt.Tx) error {
|
||||
b := storeMailbox.txGetDeletedIDsBucket(tx)
|
||||
c := b.Cursor()
|
||||
for k, _ := c.First(); k != nil; k, _ = c.Next() {
|
||||
apiIDs = append(apiIDs, string(k))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// getSequenceNumber returns IMAP sequence number in the mailbox for the message with the given API ID `apiID`.
|
||||
func (storeMailbox *Mailbox) getSequenceNumber(apiID string) (seqNum uint32, err error) {
|
||||
err = storeMailbox.db().View(func(tx *bolt.Tx) error {
|
||||
|
||||
@ -24,6 +24,8 @@ import (
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// ErrAllMailOpNotAllowed is error user when user tries to do unsupported
|
||||
// operation on All Mail folder
|
||||
var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder")
|
||||
|
||||
// GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage`
|
||||
@ -96,11 +98,8 @@ func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error {
|
||||
// It has to be propagated to all the same messages in all mailboxes.
|
||||
// The propagation is processed by the event loop.
|
||||
func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error {
|
||||
log.WithFields(logrus.Fields{
|
||||
"messages": apiIDs,
|
||||
"label": storeMailbox.labelID,
|
||||
"mailbox": storeMailbox.Name,
|
||||
}).Trace("Unlabeling messages")
|
||||
storeMailbox.log.WithField("messages", apiIDs).
|
||||
Trace("Unlabeling messages")
|
||||
if storeMailbox.labelID == pmapi.AllMailLabel {
|
||||
return ErrAllMailOpNotAllowed
|
||||
}
|
||||
@ -129,6 +128,9 @@ func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error {
|
||||
ids = append(ids, apiID)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return storeMailbox.client().MarkMessagesRead(ids)
|
||||
}
|
||||
|
||||
@ -170,56 +172,66 @@ func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error {
|
||||
return storeMailbox.client().UnlabelMessages(apiIDs, pmapi.StarredLabel)
|
||||
}
|
||||
|
||||
// DeleteMessages deletes messages.
|
||||
// If the mailbox is All Mail or All Sent, it does nothing.
|
||||
// If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted.
|
||||
// In all other cases the message is only removed from the mailbox.
|
||||
func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
|
||||
// MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API
|
||||
// until RemoveDeleted is called
|
||||
func (storeMailbox *Mailbox) MarkMessagesDeleted(apiIDs []string) error {
|
||||
log.WithFields(logrus.Fields{
|
||||
"messages": apiIDs,
|
||||
"label": storeMailbox.labelID,
|
||||
"mailbox": storeMailbox.Name,
|
||||
}).Trace("Deleting messages")
|
||||
}).Trace("Marking messages as deleted")
|
||||
if storeMailbox.labelID == pmapi.AllMailLabel {
|
||||
return ErrAllMailOpNotAllowed
|
||||
}
|
||||
return storeMailbox.store.db.Update(func(tx *bolt.Tx) error {
|
||||
return storeMailbox.txMarkMessagesAsDeleted(tx, apiIDs, true)
|
||||
})
|
||||
}
|
||||
|
||||
// MarkMessagesUndeleted removes local flag \Deleted. This is not propagated to
|
||||
// API.
|
||||
func (storeMailbox *Mailbox) MarkMessagesUndeleted(apiIDs []string) error {
|
||||
log.WithFields(logrus.Fields{
|
||||
"messages": apiIDs,
|
||||
"label": storeMailbox.labelID,
|
||||
"mailbox": storeMailbox.Name,
|
||||
}).Trace("Marking messages as undeleted")
|
||||
if storeMailbox.labelID == pmapi.AllMailLabel {
|
||||
return ErrAllMailOpNotAllowed
|
||||
}
|
||||
return storeMailbox.store.db.Update(func(tx *bolt.Tx) error {
|
||||
return storeMailbox.txMarkMessagesAsDeleted(tx, apiIDs, false)
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveDeleted sends request to API to remove message from mailbox.
|
||||
// If the mailbox is All Mail or All Sent, it does nothing.
|
||||
// If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted.
|
||||
// In all other cases the message is only removed from the mailbox.
|
||||
func (storeMailbox *Mailbox) RemoveDeleted() error {
|
||||
storeMailbox.log.Trace("Deleting messages")
|
||||
|
||||
apiIDs, err := storeMailbox.GetDeletedAPIIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(apiIDs) == 0 {
|
||||
storeMailbox.log.Debug("List to expunge is empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
defer storeMailbox.pollNow()
|
||||
|
||||
switch storeMailbox.labelID {
|
||||
case pmapi.AllMailLabel, pmapi.AllSentLabel:
|
||||
break
|
||||
case pmapi.TrashLabel, pmapi.SpamLabel:
|
||||
messageIDsToDelete := []string{}
|
||||
messageIDsToUnlabel := []string{}
|
||||
for _, apiID := range apiIDs {
|
||||
msg, err := storeMailbox.store.getMessageFromDB(apiID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
otherLabels := false
|
||||
// If the message has any custom label, we don't want to delete it, only remove trash/spam label.
|
||||
for _, label := range msg.LabelIDs {
|
||||
if label != pmapi.SpamLabel && label != pmapi.TrashLabel && label != pmapi.AllMailLabel && label != pmapi.AllSentLabel && label != pmapi.DraftLabel && label != pmapi.AllDraftsLabel {
|
||||
otherLabels = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if otherLabels {
|
||||
messageIDsToUnlabel = append(messageIDsToUnlabel, apiID)
|
||||
} else {
|
||||
messageIDsToDelete = append(messageIDsToDelete, apiID)
|
||||
}
|
||||
}
|
||||
if len(messageIDsToUnlabel) > 0 {
|
||||
if err := storeMailbox.client().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil {
|
||||
log.WithError(err).Warning("Cannot unlabel before deleting")
|
||||
}
|
||||
}
|
||||
if len(messageIDsToDelete) > 0 {
|
||||
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := storeMailbox.deleteFromTrashOrSpam(apiIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
case pmapi.DraftLabel:
|
||||
storeMailbox.log.WithField("ids", apiIDs).Warn("Deleting drafts")
|
||||
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -231,6 +243,51 @@ func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteFromTrashOrSpam will remove messages from API forever. If messages
|
||||
// still has some custom label the message will not be deleted. Instead it will
|
||||
// be removed from Trash or Spam.
|
||||
func (storeMailbox *Mailbox) deleteFromTrashOrSpam(apiIDs []string) error {
|
||||
l := storeMailbox.log.WithField("messages", apiIDs)
|
||||
l.Trace("Deleting messages from trash")
|
||||
|
||||
messageIDsToDelete := []string{}
|
||||
messageIDsToUnlabel := []string{}
|
||||
for _, apiID := range apiIDs {
|
||||
msg, err := storeMailbox.store.getMessageFromDB(apiID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
otherLabels := false
|
||||
// If the message has any custom label, we don't want to delete it, only remove trash/spam label.
|
||||
for _, label := range msg.LabelIDs {
|
||||
if label != pmapi.SpamLabel && label != pmapi.TrashLabel && label != pmapi.AllMailLabel && label != pmapi.AllSentLabel && label != pmapi.DraftLabel && label != pmapi.AllDraftsLabel {
|
||||
otherLabels = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if otherLabels {
|
||||
messageIDsToUnlabel = append(messageIDsToUnlabel, apiID)
|
||||
} else {
|
||||
messageIDsToDelete = append(messageIDsToDelete, apiID)
|
||||
}
|
||||
}
|
||||
if len(messageIDsToUnlabel) > 0 {
|
||||
if err := storeMailbox.client().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil {
|
||||
l.WithError(err).Warning("Cannot unlabel before deleting")
|
||||
}
|
||||
}
|
||||
if len(messageIDsToDelete) > 0 {
|
||||
storeMailbox.log.WithField("ids", messageIDsToDelete).Warn("Deleting messages")
|
||||
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storeMailbox *Mailbox) txSkipAndRemoveFromMailbox(tx *bolt.Tx, msg *pmapi.Message) (skipAndRemove bool) {
|
||||
defer func() {
|
||||
if skipAndRemove {
|
||||
@ -270,7 +327,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
|
||||
|
||||
// Buckets are not initialized right away because it's a heavy operation.
|
||||
// The best option is to get the same bucket only once and only when needed.
|
||||
var apiBucket, imapBucket *bolt.Bucket
|
||||
var apiBucket, imapBucket, deletedBucket *bolt.Bucket
|
||||
for _, msg := range msgs {
|
||||
if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) {
|
||||
continue
|
||||
@ -289,19 +346,23 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
|
||||
}
|
||||
} else {
|
||||
uidb := apiBucket.Get([]byte(msg.ID))
|
||||
|
||||
if uidb != nil {
|
||||
if imapBucket == nil {
|
||||
imapBucket = storeMailbox.txGetIMAPIDsBucket(tx)
|
||||
}
|
||||
seqNum, seqErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb)
|
||||
if deletedBucket == nil {
|
||||
deletedBucket = storeMailbox.txGetDeletedIDsBucket(tx)
|
||||
}
|
||||
isMarkedAsDeleted := deletedBucket.Get([]byte(msg.ID)) != nil
|
||||
if seqErr == nil {
|
||||
storeMailbox.store.imapUpdateMessage(
|
||||
storeMailbox.store.notifyUpdateMessage(
|
||||
storeMailbox.storeAddress.address,
|
||||
storeMailbox.labelName,
|
||||
btoi(uidb),
|
||||
seqNum,
|
||||
msg,
|
||||
isMarkedAsDeleted,
|
||||
)
|
||||
}
|
||||
continue
|
||||
@ -329,12 +390,13 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get sequence number from UID")
|
||||
}
|
||||
storeMailbox.store.imapUpdateMessage(
|
||||
storeMailbox.store.notifyUpdateMessage(
|
||||
storeMailbox.storeAddress.address,
|
||||
storeMailbox.labelName,
|
||||
uid,
|
||||
seqNum,
|
||||
msg,
|
||||
false, // new message is never marked as deleted
|
||||
)
|
||||
shouldSendMailboxUpdate = true
|
||||
}
|
||||
@ -359,6 +421,7 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
|
||||
}
|
||||
|
||||
imapBucket := storeMailbox.txGetIMAPIDsBucket(tx)
|
||||
deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx)
|
||||
|
||||
seqNum, seqNumErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb)
|
||||
if seqNumErr != nil {
|
||||
@ -373,8 +436,12 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
|
||||
return errors.Wrap(err, "cannot delete from API bucket")
|
||||
}
|
||||
|
||||
if err := deletedBucket.Delete(apiIDb); err != nil {
|
||||
return errors.Wrap(err, "cannot delete from mark-as-deleted bucket")
|
||||
}
|
||||
|
||||
if seqNumErr == nil {
|
||||
storeMailbox.store.imapDeleteMessage(
|
||||
storeMailbox.store.notifyDeleteMessage(
|
||||
storeMailbox.storeAddress.address,
|
||||
storeMailbox.labelName,
|
||||
seqNum,
|
||||
@ -392,7 +459,7 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get counts for mailbox status update")
|
||||
}
|
||||
storeMailbox.store.imapMailboxStatus(
|
||||
storeMailbox.store.notifyMailboxStatus(
|
||||
storeMailbox.storeAddress.address,
|
||||
storeMailbox.labelName,
|
||||
total,
|
||||
@ -401,3 +468,50 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storeMailbox *Mailbox) txMarkMessagesAsDeleted(tx *bolt.Tx, apiIDs []string, markAsDeleted bool) error {
|
||||
// Load all buckets before looping over apiIDs
|
||||
metaBucket := tx.Bucket(metadataBucket)
|
||||
apiBucket := storeMailbox.txGetAPIIDsBucket(tx)
|
||||
uidBucket := storeMailbox.txGetIMAPIDsBucket(tx)
|
||||
deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx)
|
||||
for _, apiID := range apiIDs {
|
||||
if markAsDeleted {
|
||||
if err := deletedBucket.Put([]byte(apiID), []byte{1}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := deletedBucket.Delete([]byte(apiID)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
msg, err := storeMailbox.store.txGetMessageFromBucket(metaBucket, apiID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uid, err := storeMailbox.txGetUIDFromBucket(apiBucket, apiID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
seqNum, err := storeMailbox.txGetSequenceNumberOfUID(uidBucket, itob(uid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// In order to send flags in format
|
||||
// S: * 2 FETCH (FLAGS (\Deleted \Seen))
|
||||
storeMailbox.store.notifyUpdateMessage(
|
||||
storeMailbox.storeAddress.address,
|
||||
storeMailbox.labelName,
|
||||
uid,
|
||||
seqNum,
|
||||
msg,
|
||||
markAsDeleted,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -62,6 +62,21 @@ func (message *Message) Message() *pmapi.Message {
|
||||
return message.msg
|
||||
}
|
||||
|
||||
// IsMarkedDeleted returns true if message is marked as deleted for specific
|
||||
// mailbox
|
||||
func (message *Message) IsMarkedDeleted() bool {
|
||||
isMarkedAsDeleted := false
|
||||
err := message.storeMailbox.db().View(func(tx *bolt.Tx) error {
|
||||
isMarkedAsDeleted = message.storeMailbox.txGetDeletedIDsBucket(tx).Get([]byte(message.msg.ID)) != nil
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
message.storeMailbox.log.WithError(err).Error("Not able to retrieve deleted mark, assuming false.")
|
||||
return false
|
||||
}
|
||||
return isMarkedAsDeleted
|
||||
}
|
||||
|
||||
// SetSize updates the information about size of decrypted message which can be
|
||||
// used for IMAP. This should not trigger any IMAP update.
|
||||
// NOTE: The size from the server corresponds to pure body bytes. Hence it
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// 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
|
||||
@ -105,6 +105,18 @@ func (m *MockBridgeUser) EXPECT() *MockBridgeUserMockRecorder {
|
||||
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
|
||||
func (m *MockBridgeUser) CloseConnection(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -229,3 +241,86 @@ func (mr *MockBridgeUserMockRecorder) UpdateUser() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
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)
|
||||
}
|
||||
|
||||
@ -5,9 +5,10 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockListener is a mock of Listener interface
|
||||
|
||||
@ -26,7 +26,6 @@ import (
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
imapBackend "github.com/emersion/go-imap/backend"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -70,6 +69,8 @@ var (
|
||||
// * {imapUID} -> string messageID
|
||||
// * api_ids
|
||||
// * {messageID} -> uint32 imapUID
|
||||
// * deleted_ids (can be missing or have no keys)
|
||||
// * {messageID} -> true
|
||||
metadataBucket = []byte("metadata") //nolint[gochecknoglobals]
|
||||
countsBucket = []byte("counts") //nolint[gochecknoglobals]
|
||||
addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals]
|
||||
@ -78,6 +79,7 @@ var (
|
||||
mailboxesBucket = []byte("mailboxes") //nolint[gochecknoglobals]
|
||||
imapIDsBucket = []byte("imap_ids") //nolint[gochecknoglobals]
|
||||
apiIDsBucket = []byte("api_ids") //nolint[gochecknoglobals]
|
||||
deletedIDsBucket = []byte("deleted_ids") //nolint[gochecknoglobals]
|
||||
mboxVersionBucket = []byte("mailboxes_version") //nolint[gochecknoglobals]
|
||||
|
||||
// ErrNoSuchAPIID when mailbox does not have API ID.
|
||||
@ -97,12 +99,12 @@ type Store struct {
|
||||
|
||||
log *logrus.Entry
|
||||
|
||||
cache *Cache
|
||||
filePath string
|
||||
db *bolt.DB
|
||||
lock *sync.RWMutex
|
||||
addresses map[string]*Address
|
||||
imapUpdates chan imapBackend.Update
|
||||
cache *Cache
|
||||
filePath string
|
||||
db *bolt.DB
|
||||
lock *sync.RWMutex
|
||||
addresses map[string]*Address
|
||||
notifier ChangeNotifier
|
||||
|
||||
isSyncRunning bool
|
||||
syncCooldown cooldown
|
||||
@ -348,6 +350,18 @@ func (store *Store) addAddress(address, addressID string, labels []*pmapi.Label)
|
||||
return
|
||||
}
|
||||
|
||||
// PauseEventLoop sets whether the ticker is periodically polling or not.
|
||||
func (store *Store) PauseEventLoop(pause bool) {
|
||||
store.lock.Lock()
|
||||
defer store.lock.Unlock()
|
||||
|
||||
store.log.WithField("pause", pause).Info("Pausing event loop")
|
||||
|
||||
if store.eventLoop != nil {
|
||||
store.eventLoop.isTickerPaused = pause
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the event loop and closes the database to free the file.
|
||||
func (store *Store) Close() error {
|
||||
store.lock.Lock()
|
||||
|
||||
@ -18,11 +18,12 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
storemocks "github.com/ProtonMail/proton-bridge/internal/store/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -43,13 +44,14 @@ const (
|
||||
type mocksForStore struct {
|
||||
tb testing.TB
|
||||
|
||||
ctrl *gomock.Controller
|
||||
events *storemocks.MockListener
|
||||
user *storemocks.MockBridgeUser
|
||||
client *pmapimocks.MockClient
|
||||
clientManager *storemocks.MockClientManager
|
||||
panicHandler *storemocks.MockPanicHandler
|
||||
store *Store
|
||||
ctrl *gomock.Controller
|
||||
events *storemocks.MockListener
|
||||
user *storemocks.MockBridgeUser
|
||||
client *pmapimocks.MockClient
|
||||
clientManager *storemocks.MockClientManager
|
||||
panicHandler *storemocks.MockPanicHandler
|
||||
changeNotifier *storemocks.MockChangeNotifier
|
||||
store *Store
|
||||
|
||||
tmpDir string
|
||||
cache *Cache
|
||||
@ -58,13 +60,14 @@ type mocksForStore struct {
|
||||
func initMocks(tb testing.TB) (*mocksForStore, func()) {
|
||||
ctrl := gomock.NewController(tb)
|
||||
mocks := &mocksForStore{
|
||||
tb: tb,
|
||||
ctrl: ctrl,
|
||||
events: storemocks.NewMockListener(ctrl),
|
||||
user: storemocks.NewMockBridgeUser(ctrl),
|
||||
client: pmapimocks.NewMockClient(ctrl),
|
||||
clientManager: storemocks.NewMockClientManager(ctrl),
|
||||
panicHandler: storemocks.NewMockPanicHandler(ctrl),
|
||||
tb: tb,
|
||||
ctrl: ctrl,
|
||||
events: storemocks.NewMockListener(ctrl),
|
||||
user: storemocks.NewMockBridgeUser(ctrl),
|
||||
client: pmapimocks.NewMockClient(ctrl),
|
||||
clientManager: storemocks.NewMockClientManager(ctrl),
|
||||
panicHandler: storemocks.NewMockPanicHandler(ctrl),
|
||||
changeNotifier: storemocks.NewMockChangeNotifier(ctrl),
|
||||
}
|
||||
|
||||
// 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().IsConnected().Return(true)
|
||||
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().CountMessages("")
|
||||
mocks.client.EXPECT().GetEvent(gomock.Any()).
|
||||
Return(&pmapi.Event{
|
||||
EventID: "latestEventID",
|
||||
}, nil).AnyTimes()
|
||||
|
||||
// We want to wait until first sync has finished.
|
||||
firstSyncWaiter := sync.WaitGroup{}
|
||||
firstSyncWaiter.Add(1)
|
||||
mocks.client.EXPECT().
|
||||
ListMessages(gomock.Any()).
|
||||
DoAndReturn(func(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
|
||||
firstSyncWaiter.Done()
|
||||
return []*pmapi.Message{}, 0, nil
|
||||
})
|
||||
// Call to get latest event ID and then to process first event.
|
||||
mocks.client.EXPECT().GetEvent("").Return(&pmapi.Event{
|
||||
EventID: "firstEventID",
|
||||
}, nil)
|
||||
mocks.client.EXPECT().GetEvent("firstEventID").Return(&pmapi.Event{
|
||||
EventID: "latestEventID",
|
||||
}, 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
|
||||
mocks.store, err = New(
|
||||
@ -128,6 +130,16 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool) { //nolint[unpar
|
||||
)
|
||||
require.NoError(mocks.tb, err)
|
||||
|
||||
// Wait for sync to finish.
|
||||
firstSyncWaiter.Wait()
|
||||
// We want to wait until first sync has finished.
|
||||
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)
|
||||
}
|
||||
|
||||
@ -26,6 +26,10 @@ import (
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func (loop *eventLoop) IsRunning() bool {
|
||||
return loop.isRunning
|
||||
}
|
||||
|
||||
// TestSync triggers a sync of the store.
|
||||
func (store *Store) TestSync() {
|
||||
store.lock.Lock()
|
||||
@ -102,11 +106,13 @@ func txDumpMailsFactory(tb assert.TestingT) func(tx *bolt.Tx) error {
|
||||
err := mailboxes.ForEach(func(mboxName, mboxData []byte) error {
|
||||
fmt.Println("mbox:", string(mboxName))
|
||||
b := mailboxes.Bucket(mboxName).Bucket(imapIDsBucket)
|
||||
deletedMailboxes := mailboxes.Bucket(mboxName).Bucket(deletedIDsBucket)
|
||||
c := b.Cursor()
|
||||
i := 0
|
||||
for imapID, apiID := c.First(); imapID != nil; imapID, apiID = c.Next() {
|
||||
i++
|
||||
fmt.Println(" ", i, "imap", btoi(imapID), "api", string(apiID))
|
||||
isDeleted := deletedMailboxes != nil && deletedMailboxes.Get(apiID) != nil
|
||||
fmt.Println(" ", i, "imap", btoi(imapID), "api", string(apiID), "isDeleted", isDeleted)
|
||||
data := metadata.Get(apiID)
|
||||
if !assert.NotNil(tb, data) {
|
||||
continue
|
||||
|
||||
@ -36,6 +36,7 @@ type BridgeUser interface {
|
||||
GetPrimaryAddress() string
|
||||
GetStoreAddresses() []string
|
||||
UpdateUser() error
|
||||
CloseAllConnections()
|
||||
CloseConnection(string)
|
||||
Logout() error
|
||||
}
|
||||
|
||||
@ -210,7 +210,9 @@ func (store *Store) deleteMailboxEvent(labelID string) error {
|
||||
store.lock.Lock()
|
||||
defer store.lock.Unlock()
|
||||
|
||||
_ = store.removeMailboxCount(labelID)
|
||||
if err := store.removeMailboxCount(labelID); err != nil {
|
||||
log.WithError(err).Warn("Problem to remove mailbox counts while deleting mailbox")
|
||||
}
|
||||
|
||||
for _, a := range store.addresses {
|
||||
if err := a.deleteMailboxEvent(labelID); err != nil {
|
||||
|
||||
@ -63,7 +63,7 @@ func (store *Store) CreateDraft(
|
||||
attachmentReaders = append(attachmentReaders, strings.NewReader(attachedPublicKey))
|
||||
publicKeyAttachment := &pmapi.Attachment{
|
||||
Name: attachedPublicKeyName + ".asc",
|
||||
MIMEType: "application/pgp-key",
|
||||
MIMEType: "application/pgp-keys",
|
||||
Header: textproto.MIMEHeader{},
|
||||
}
|
||||
attachments = append(attachments, publicKeyAttachment)
|
||||
@ -143,8 +143,10 @@ func (store *Store) getMessageFromDB(apiID string) (msg *pmapi.Message, err erro
|
||||
}
|
||||
|
||||
func (store *Store) txGetMessage(tx *bolt.Tx, apiID string) (*pmapi.Message, error) {
|
||||
b := tx.Bucket(metadataBucket)
|
||||
return store.txGetMessageFromBucket(tx.Bucket(metadataBucket), apiID)
|
||||
}
|
||||
|
||||
func (store *Store) txGetMessageFromBucket(b *bolt.Bucket, apiID string) (*pmapi.Message, error) {
|
||||
msgb := b.Get([]byte(apiID))
|
||||
if msgb == nil {
|
||||
return nil, ErrNoSuchAPIID
|
||||
|
||||
@ -29,18 +29,36 @@ type Message struct {
|
||||
ID string
|
||||
Unread bool
|
||||
Body []byte
|
||||
Source Mailbox
|
||||
Sources []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.
|
||||
type MessageStatus struct {
|
||||
eventTime time.Time // Time of adding message to the process.
|
||||
rule *Rule // Rule with source and target mailboxes.
|
||||
SourceID string // Message ID at the source.
|
||||
targetID string // Message ID at the target (if any).
|
||||
bodyHash string // Hash of the message body.
|
||||
eventTime time.Time // Time of adding message to the process.
|
||||
sourceNames []string // Source mailbox names message is in.
|
||||
SourceID string // Message ID at the source.
|
||||
targetNames []string // Target mailbox names message is in.
|
||||
targetID string // Message ID at the target (if any).
|
||||
bodyHash string // Hash of the message body.
|
||||
|
||||
skipped bool
|
||||
exported bool
|
||||
imported bool
|
||||
exportErr error
|
||||
@ -79,7 +97,7 @@ func (status *MessageStatus) setDetailsFromHeader(header mail.Header) {
|
||||
}
|
||||
|
||||
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.
|
||||
@ -88,6 +106,9 @@ func (status *MessageStatus) GetErrorMessage() string {
|
||||
}
|
||||
|
||||
func (status *MessageStatus) getErrorMessage(includeMissing bool) string {
|
||||
if status.skipped {
|
||||
return ""
|
||||
}
|
||||
if status.exportErr != nil {
|
||||
return fmt.Sprintf("failed to export: %s", status.exportErr)
|
||||
}
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
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
|
||||
@ -95,3 +98,170 @@ func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Cal
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
|
||||
}
|
||||
|
||||
// MockIMAPClientProvider is a mock of IMAPClientProvider interface
|
||||
type MockIMAPClientProvider struct {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -62,11 +62,20 @@ func (p *Progress) update() {
|
||||
return
|
||||
}
|
||||
|
||||
// In case no one listens for an update, do not block the progress.
|
||||
select {
|
||||
case p.updateCh <- struct{}{}:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
// In case no one listens for an update, do not block the whole progress.
|
||||
go func() {
|
||||
defer func() {
|
||||
// updateCh can be closed at the end of progress which is fine.
|
||||
if r := recover(); r != nil {
|
||||
log.WithField("r", r).Warn("Failed to send update")
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case p.updateCh <- struct{}{}:
|
||||
case <-time.After(5 * time.Millisecond):
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// finish should be called as the last call once everything is done.
|
||||
@ -84,7 +93,7 @@ func (p *Progress) fatal(err error) {
|
||||
defer p.lock.Unlock()
|
||||
|
||||
log.WithError(err).Error("Progress finished")
|
||||
p.isStopped = true
|
||||
p.setStop()
|
||||
p.fatalError = err
|
||||
p.cleanUpdateCh()
|
||||
}
|
||||
@ -117,19 +126,33 @@ func (p *Progress) updateCount(mailbox string, count uint) {
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
p.log.WithField("id", messageID).Trace("Message added")
|
||||
p.messageStatuses[messageID] = &MessageStatus{
|
||||
eventTime: time.Now(),
|
||||
rule: rule,
|
||||
SourceID: messageID,
|
||||
eventTime: time.Now(),
|
||||
sourceNames: sourceNames,
|
||||
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.
|
||||
func (p *Progress) messageExported(messageID string, body []byte, err error) {
|
||||
p.lock.Lock()
|
||||
@ -273,6 +296,15 @@ func (p *Progress) Stop() {
|
||||
defer p.update()
|
||||
|
||||
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.pauseReason = "" // Clear pause to run paused code and stop it.
|
||||
}
|
||||
@ -311,35 +343,40 @@ func (p *Progress) GetFailedMessages() []*MessageStatus {
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
counts := ProgressCounts{}
|
||||
|
||||
// Return counts only once total is estimated or the process already
|
||||
// ended (for a case when it ended quickly to report it correctly).
|
||||
if p.updateCh != nil && !p.messageCounted {
|
||||
return
|
||||
return counts
|
||||
}
|
||||
|
||||
// Include lost messages in the process only when transfer is done.
|
||||
includeMissing := p.updateCh == nil
|
||||
|
||||
for _, mailboxCount := range p.messageCounts {
|
||||
total += mailboxCount
|
||||
counts.Total += mailboxCount
|
||||
}
|
||||
for _, status := range p.messageStatuses {
|
||||
added++
|
||||
counts.Added++
|
||||
if status.skipped {
|
||||
counts.Skipped++
|
||||
}
|
||||
if status.exported {
|
||||
exported++
|
||||
counts.Exported++
|
||||
}
|
||||
if status.imported {
|
||||
imported++
|
||||
counts.Imported++
|
||||
}
|
||||
if status.hasError(includeMissing) {
|
||||
failed++
|
||||
counts.Failed++
|
||||
}
|
||||
}
|
||||
return
|
||||
return counts
|
||||
}
|
||||
|
||||
// GenerateBugReport generates similar file to import log except private information.
|
||||
|
||||
35
internal/transfer/progress_counts.go
Normal file
35
internal/transfer/progress_counts.go
Normal 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)
|
||||
}
|
||||
@ -19,6 +19,7 @@ package transfer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
a "github.com/stretchr/testify/assert"
|
||||
@ -38,8 +39,8 @@ func TestProgressUpdateCount(t *testing.T) {
|
||||
|
||||
progress.finish()
|
||||
|
||||
_, _, _, _, total := progress.GetCounts() //nolint[dogsled]
|
||||
r.Equal(t, uint(42), total)
|
||||
counts := progress.GetCounts()
|
||||
r.Equal(t, uint(42), counts.Total)
|
||||
}
|
||||
|
||||
func TestProgressAddingMessages(t *testing.T) {
|
||||
@ -47,31 +48,36 @@ func TestProgressAddingMessages(t *testing.T) {
|
||||
drainProgressUpdateChannel(&progress)
|
||||
|
||||
// msg1 has no problem.
|
||||
progress.addMessage("msg1", nil)
|
||||
progress.addMessage("msg1", []string{}, []string{})
|
||||
progress.messageExported("msg1", []byte(""), nil)
|
||||
progress.messageImported("msg1", "", nil)
|
||||
|
||||
// msg2 has an import problem.
|
||||
progress.addMessage("msg2", nil)
|
||||
progress.addMessage("msg2", []string{}, []string{})
|
||||
progress.messageExported("msg2", []byte(""), nil)
|
||||
progress.messageImported("msg2", "", errors.New("failed import"))
|
||||
|
||||
// msg3 has an export problem.
|
||||
progress.addMessage("msg3", nil)
|
||||
progress.addMessage("msg3", []string{}, []string{})
|
||||
progress.messageExported("msg3", []byte(""), errors.New("failed export"))
|
||||
|
||||
// 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.messageImported("msg4", "", nil)
|
||||
|
||||
// msg5 is skipped.
|
||||
progress.addMessage("msg5", []string{}, []string{})
|
||||
progress.messageSkipped("msg5")
|
||||
|
||||
progress.finish()
|
||||
|
||||
failed, imported, exported, added, _ := progress.GetCounts()
|
||||
a.Equal(t, uint(4), added)
|
||||
a.Equal(t, uint(2), exported)
|
||||
a.Equal(t, uint(2), imported)
|
||||
a.Equal(t, uint(3), failed)
|
||||
counts := progress.GetCounts()
|
||||
a.Equal(t, uint(5), counts.Added)
|
||||
a.Equal(t, uint(2), counts.Exported)
|
||||
a.Equal(t, uint(2), counts.Imported)
|
||||
a.Equal(t, uint(1), counts.Skipped)
|
||||
a.Equal(t, uint(3), counts.Failed)
|
||||
|
||||
errorsMap := map[string]string{}
|
||||
for _, status := range progress.GetFailedMessages() {
|
||||
@ -91,7 +97,7 @@ func TestProgressFinish(t *testing.T) {
|
||||
progress.finish()
|
||||
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) {
|
||||
@ -101,7 +107,29 @@ func TestProgressFatalError(t *testing.T) {
|
||||
progress.fatal(errors.New("fatal error"))
|
||||
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) {
|
||||
|
||||
@ -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) {
|
||||
count := uint(len(filePaths))
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
if progress.shouldStop() {
|
||||
break
|
||||
@ -91,6 +89,8 @@ func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *P
|
||||
|
||||
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
|
||||
// to not waste energy.
|
||||
if err == nil && rule.HasTimeLimit() {
|
||||
@ -99,17 +99,11 @@ func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *P
|
||||
err = msgTimeErr
|
||||
} else if !rule.isTimeInRange(msgTime) {
|
||||
log.WithField("msg", filePath).Debug("Message skipped due to time")
|
||||
|
||||
count--
|
||||
progress.updateCount(rule.SourceMailbox.Name, count)
|
||||
progress.messageSkipped(filePath)
|
||||
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)
|
||||
if err == nil {
|
||||
ch <- msg
|
||||
@ -134,7 +128,7 @@ func (p *EMLProvider) exportMessage(rule *Rule, filePath string) (Message, error
|
||||
ID: filePath,
|
||||
Unread: false,
|
||||
Body: body,
|
||||
Source: rule.SourceMailbox,
|
||||
Sources: []Mailbox{rule.SourceMailbox},
|
||||
Targets: rule.TargetMailboxes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ func (p *EMLProvider) TransferFrom(rules transferRules, progress *Progress, ch <
|
||||
func (p *EMLProvider) createFolders(rules transferRules) error {
|
||||
for rule := range rules.iterateActiveRules() {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -71,7 +71,7 @@ func (p *EMLProvider) createFolders(rules transferRules) error {
|
||||
}
|
||||
|
||||
func (p *EMLProvider) writeFile(msg Message) error {
|
||||
fileName := filepath.Base(msg.ID)
|
||||
fileName := sanitizeFileName(filepath.Base(msg.ID))
|
||||
if filepath.Ext(fileName) != ".eml" {
|
||||
fileName += ".eml"
|
||||
}
|
||||
|
||||
@ -21,24 +21,49 @@ import (
|
||||
"net"
|
||||
"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.
|
||||
type IMAPProvider struct {
|
||||
username string
|
||||
password string
|
||||
addr string
|
||||
|
||||
client *imapClient.Client
|
||||
clientDialer func(addr string) (IMAPClientProvider, error)
|
||||
client IMAPClientProvider
|
||||
|
||||
timeIt *timeIt
|
||||
}
|
||||
|
||||
// NewIMAPProvider returns new IMAPProvider.
|
||||
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{
|
||||
username: username,
|
||||
password: password,
|
||||
addr: net.JoinHostPort(host, port),
|
||||
|
||||
timeIt: newTimeIt("imap"),
|
||||
|
||||
clientDialer: clientDialer,
|
||||
}
|
||||
|
||||
if err := p.auth(); err != nil {
|
||||
|
||||
@ -40,6 +40,9 @@ func (p *IMAPProvider) TransferTo(rules transferRules, progress *Progress, ch ch
|
||||
log.Info("Started transfer from IMAP to channel")
|
||||
defer log.Info("Finished transfer from IMAP to channel")
|
||||
|
||||
p.timeIt.clear()
|
||||
defer p.timeIt.logResults()
|
||||
|
||||
imapMessageInfoMap := p.loadMessageInfoMap(rules, progress)
|
||||
|
||||
for rule := range rules.iterateActiveRules() {
|
||||
@ -78,12 +81,40 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres
|
||||
}
|
||||
|
||||
func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValidity, count uint32) map[string]imapMessageInfo {
|
||||
p.timeIt.start("load", rule.SourceMailbox.Name)
|
||||
defer p.timeIt.stop("load", rule.SourceMailbox.Name)
|
||||
|
||||
log := log.WithField("mailbox", rule.SourceMailbox.Name)
|
||||
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)
|
||||
pageEnd := imapPageSize
|
||||
for {
|
||||
if progress.shouldStop() {
|
||||
if progress.shouldStop() || pageStart > count {
|
||||
break
|
||||
}
|
||||
|
||||
@ -94,45 +125,21 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
|
||||
|
||||
seqSet := &imap.SeqSet{}
|
||||
seqSet.AddRange(pageStart, pageEnd)
|
||||
|
||||
items := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size}
|
||||
if rule.HasTimeLimit() {
|
||||
items = append(items, imap.FetchEnvelope)
|
||||
}
|
||||
|
||||
pageMsgCount := uint32(0)
|
||||
processMessageCallback := func(imapMessage *imap.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
|
||||
err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("idx", seqSet).Warning("Load batch fetch failed, trying one by one")
|
||||
for ; pageStart <= pageEnd; pageStart++ {
|
||||
seqSet := &imap.SeqSet{}
|
||||
seqSet.AddNum(pageStart)
|
||||
if err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback); err != nil {
|
||||
log.WithError(err).WithField("idx", seqSet).Warning("Load fetch failed, skipping the message")
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return p.fetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
|
||||
})
|
||||
|
||||
if pageMsgCount < imapPageSize {
|
||||
break
|
||||
}
|
||||
|
||||
pageStart = pageEnd
|
||||
pageStart = pageEnd + 1
|
||||
pageEnd += imapPageSize
|
||||
}
|
||||
|
||||
return messagesInfo
|
||||
}
|
||||
|
||||
@ -199,13 +206,18 @@ func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<-
|
||||
progress.messageExported(id, body, err)
|
||||
if err == nil {
|
||||
msg := p.exportMessage(rule, id, imapMessage, body)
|
||||
|
||||
p.timeIt.stop("fetch", rule.SourceMailbox.Name)
|
||||
ch <- msg
|
||||
p.timeIt.start("fetch", rule.SourceMailbox.Name)
|
||||
}
|
||||
}
|
||||
|
||||
p.timeIt.start("fetch", rule.SourceMailbox.Name)
|
||||
progress.callWrap(func() error {
|
||||
return p.uidFetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
|
||||
})
|
||||
p.timeIt.stop("fetch", rule.SourceMailbox.Name)
|
||||
}
|
||||
|
||||
func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Message, body []byte) Message {
|
||||
@ -220,7 +232,7 @@ func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Me
|
||||
ID: id,
|
||||
Unread: unread,
|
||||
Body: body,
|
||||
Source: rule.SourceMailbox,
|
||||
Sources: []Mailbox{rule.SourceMailbox},
|
||||
Targets: rule.TargetMailboxes,
|
||||
}
|
||||
}
|
||||
|
||||
100
internal/transfer/provider_imap_test.go
Normal file
100
internal/transfer/provider_imap_test.go
Normal 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])
|
||||
}
|
||||
}
|
||||
@ -18,14 +18,17 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
imapID "github.com/ProtonMail/go-imap-id"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-imap"
|
||||
imapClient "github.com/emersion/go-imap/client"
|
||||
sasl "github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -49,6 +52,43 @@ func (l *imapErrorLogger) Println(v ...interface{}) {
|
||||
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 {
|
||||
return p.ensureConnectionAndSelection(callback, "")
|
||||
}
|
||||
@ -136,29 +176,10 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
|
||||
|
||||
log.Info("Connecting to server")
|
||||
|
||||
if _, err := net.DialTimeout("tcp", p.addr, imapDialTimeout); err != nil {
|
||||
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 {
|
||||
client, err = imapClient.DialTLS(p.addr, nil)
|
||||
}
|
||||
client, err := p.clientDialer(p.addr)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
log.Info("Connected")
|
||||
@ -196,13 +217,15 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
|
||||
|
||||
log.Info("Logged in")
|
||||
|
||||
idClient := imapID.NewClient(p.client)
|
||||
if ok, err := idClient.SupportID(); err == nil && ok {
|
||||
serverID, err := idClient.ID(imapID.ID{
|
||||
imapID.FieldName: "ImportExport",
|
||||
imapID.FieldVersion: "beta",
|
||||
})
|
||||
log.WithField("ID", serverID).WithError(err).Debug("Server info")
|
||||
if c, ok := p.client.(*imapClient.Client); ok {
|
||||
idClient := imapID.NewClient(c)
|
||||
if ok, err := idClient.SupportID(); err == nil && ok {
|
||||
serverID, err := idClient.ID(imapID.ID{
|
||||
imapID.FieldName: "ImportExport",
|
||||
imapID.FieldVersion: constants.Version,
|
||||
})
|
||||
log.WithField("ID", serverID).WithError(err).Debug("Server info")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user