Compare commits

...

152 Commits

Author SHA1 Message Date
9a77650004 Bridge GoldenGate 1.5.0
- Ensured better message flow by refactoring both address and date parsing
- Improved secure connectivity checks
- Better deb packaging
- More robust error handling

- Ensured that conversations are properly threaded
- Fixed Linux font issues (Fedora)
- Better handling of Mime encrypted messages
2020-11-04 12:26:07 +01:00
f1d70361c9 Do not include conversation ID in references 2020-11-04 09:12:16 +00:00
3496599723 feat: custom address/date parser based on rfc5322 abnf 2020-11-03 16:21:06 +01:00
9e0635a6a4 fix: don't check tls fingerprints when checking connectivity 2020-11-02 13:38:39 +00:00
10509621ce Updated go-mbox dependency back to upstream 2020-11-02 10:32:21 +01:00
3727ecdfe5 Show in error counts also lost messages at the end report 2020-10-30 13:58:32 +00:00
ac71d22e86 Waiting for unilateral update during deleting the message 2020-10-30 13:42:04 +00:00
bc81356d53 test: update feature file to use new "seq" command 2020-10-29 13:10:54 +01:00
881cb64beb Release Danube: notes, version bump, change log 2020-10-29 12:57:59 +01:00
1286e57b63 Support Apple Mail MBOX export format 2020-10-29 09:07:37 +01:00
fe5f73d96e Fix crash when IMAP client connects while account is logging in 2020-10-29 07:21:45 +00:00
8f7a8b31a3 Apply 1 suggestion(s) to 1 file(s) 2020-10-28 16:42:57 +00:00
68db35d5d4 Not able to update I-E on mac GODT-794
Added missing signal, corrected the update name, log tweaks.
2020-10-28 16:42:57 +00:00
df17017ced Apply 1 suggestion(s) to 1 file(s) 2020-10-28 10:20:32 +00:00
5c48332b0e change rectangle to column in global settings GODT-677 2020-10-27 10:13:08 +01:00
8985738af5 Merge master into devel 2020-10-23 10:31:08 +02:00
2d8a676dd5 Merge branch 'release/forth' into release/danube 2020-10-22 18:00:33 +02:00
7e0a9f398c I/E Fix printing zero time in error report 2020-10-22 09:12:56 +00:00
9af5769510 Apply 1 suggestion(s) to 1 file(s) 2020-10-22 08:26:35 +00:00
bb46d9a009 README and BUILD info about Import-Export and tags 2020-10-22 10:22:00 +02:00
606b42a6e7 Fix flaky TestFailUnpauseAndStops 2020-10-22 10:04:22 +02:00
d547f5ea22 Changelog 2020-10-21 13:56:55 +02:00
563b4889e3 Update go-imap dependency to get fix for UTF-7 incompatibility 2020-10-21 09:15:42 +00:00
b449beb68c Do not spam sentry with bad ID by integration test 2020-10-21 08:38:54 +00:00
f9d58f4f9c Merge branch 'release/forth' into release/danube 2020-10-21 09:07:27 +02:00
1dfec9902e gofmt fix 2020-10-21 09:04:06 +02:00
79cafee2eb Support quoted printable and filter out some auto-generated Gmail labels 2020-10-21 09:04:06 +02:00
64fbcdc1ca Fix mbox scanning 2020-10-21 09:04:06 +02:00
e4a341af3a Better log message 2020-10-21 09:04:05 +02:00
e0292fe957 Use map instead of list as set 2020-10-21 09:04:05 +02:00
ef85c8df24 Detect Gmail labels from All Mail mbox export 2020-10-21 09:04:05 +02:00
719d369c2a Fix transfer stopping 2020-10-21 06:42:54 +00:00
51b6f95342 Show fatal errors after export is terminated 2020-10-21 06:14:39 +00:00
26fb1fc34d Sanizize mailbox name for exporting 2020-10-21 06:02:02 +00:00
ae1578a5e2 GODT-829 fix apple mail subfolders 2020-10-20 19:09:59 +02:00
cfd8e56277 Do not resume paused transfer progress after dismissing cancel popup 2020-10-19 10:25:52 +02:00
4893931a8d Fix deadlock in integration tests for Import-Export 2020-10-16 10:53:44 +02:00
932928ddc8 Allow to send calendar update multiple times 2020-10-15 13:11:40 +00:00
a33e414f01 Do not mix font awesome icon with regular text to avoid issues on Fedora 2020-10-15 12:48:09 +00:00
43d54c8f4f Clear separation of different message IDs in integration tests 2020-10-14 14:41:39 +02:00
6cbc11a75d Fix update on windows 2020-10-14 11:25:19 +02:00
a21bb130e1 Append duplicate of emails with References 2020-10-14 10:11:49 +02:00
12403785af fix: replace, don't add, transfer encoding when making body 7-bit clean 2020-10-09 13:55:37 +02:00
b4892855d4 Set flags by FLAGS (not using +/-FLAGS) do not change spam state 2020-10-06 08:42:33 +00:00
7ff67f2217 Reverted sending IMAP updates to be not blocking again 2020-10-05 11:33:16 +02:00
4912c27be8 Changelog 2020-10-05 10:51:11 +02:00
288ba11452 test: add test for sending pgp/mime as plaintext 2020-10-01 16:56:38 +02:00
7874183052 fix(GODT-770): handle extraneous end-of-mail 2020-10-01 16:16:15 +02:00
b12873f1df Fix of speed of checking whether message is deleted 2020-10-01 13:42:16 +00:00
dc9851f8ea fix(GODT-749): don't force pgp/inline when sending plaintext 2020-10-01 10:47:39 +02:00
ec73170e9b Use label.Path instead of Name 2020-09-30 09:38:35 +02:00
68616e470c chore: bump crypto version 2020-09-25 15:45:29 +02:00
53cd2ff524 CI artifacts only for a day 2020-09-25 11:29:45 +02:00
51c8bceed8 Changelog and use pmmime 2020-09-24 18:19:35 +02:00
e02c7c7f06 Parsing message with empty address as '<>' 2020-09-24 15:37:21 +02:00
15c1d7bc24 fix: duplicate charset param 2020-09-24 14:26:45 +02:00
a89a3f6612 Convert to UTF-8 any message part which specifies charset 2020-09-24 13:58:15 +02:00
d956b04062 Parsing non-utf8 multipart/alternative message 2020-09-24 13:17:38 +02:00
ef1671d4ab Parsing message with empty CC 2020-09-24 12:23:31 +02:00
fe926cbd57 IE release notes and GODT-738 2020-09-23 13:50:08 +02:00
e01747e3b9 Merge branch 'release/forth' into devel 2020-09-23 13:10:11 +02:00
85220848d0 Update total even if its zero 2020-09-23 09:24:58 +02:00
70f91ae55b notes and build v1.4.0 2020-09-21 13:29:33 +02:00
a73b30ed9e Better naming 2020-09-18 10:25:14 +02:00
7337f78d4a PMAPI target - parallel upload 2020-09-18 10:25:14 +02:00
9b5da91f7c Fix: Yahoo not supporting TLS1.3 GODT-730 2020-09-18 07:53:53 +00:00
c7669b950f fix: gitignore should also ignore ie build files 2020-09-17 14:33:46 +02:00
b3ed8d51a7 fix: version check for catalina 2020-09-17 11:35:05 +00:00
60b7d980f4 Fix integration test - deleting from All Mail 2020-09-17 10:19:55 +00:00
abf2238e6f Wrap imap-id with backend caller 2020-09-17 08:59:28 +00:00
b4a358c084 User agent detected by fake IMAP extension instead of AUTH callback 2020-09-17 08:59:28 +00:00
3606a0ab9f QA build with option to change API URL by ENV variable 2020-09-17 08:30:31 +00:00
c5665d0dd7 Unsilent errors reading mbox files 2020-09-16 15:51:08 +02:00
d6464c0048 Fixes after rebase 2020-09-16 09:51:58 +00:00
5496a26f73 Finish tests for moving without MOVE support 2020-09-16 09:51:57 +00:00
ec9a799fe9 test - move like outlook - GODT-536 2020-09-16 09:51:57 +00:00
730abadfc3 Do not allow deleting messages from All Mail 2020-09-16 09:51:57 +00:00
60e1548685 log both timeouts in update send 2020-09-16 09:51:57 +00:00
7430c7f1f5 Timeout for sending IMAP update 2020-09-16 09:51:57 +00:00
6671b78799 Simplified integration tests 2020-09-16 09:51:57 +00:00
c7578cf53c \Deleted flag support finish 2020-09-16 09:51:57 +00:00
66e04dd5ed Implement deleted flag GODT-461 2020-09-16 09:51:57 +00:00
803353e300 Tests for deleted flag GODT-496 2020-09-16 09:51:57 +00:00
f3773c9d78 I/E measurements 2020-09-16 09:29:13 +00:00
41ac61bbe8 fix: less spammy go-message logs 2020-09-15 09:37:29 +00:00
0d3d6747ac fix: grammar in gui 2020-09-15 08:51:02 +00:00
eaa9a458c4 test: use actual broken eml 2020-09-15 06:31:45 +00:00
46e5cb9c83 test: use message.Parse for fakeapi import parser 2020-09-15 06:31:45 +00:00
dc5387a512 fix: bug report window title 2020-09-15 08:04:51 +02:00
4b7c234e78 feat: strip comments from addresses 2020-09-14 14:46:44 +02:00
5bca6fc3cf chore: tidy up before merge 2020-09-14 14:19:35 +02:00
97b64ebb70 fix: credits and release notes 2020-09-11 11:41:03 +02:00
9b3cc9dc34 feat: convert content type in html meta tags 2020-09-11 11:41:03 +02:00
afeed4a801 feat: use upstream go-message 2020-09-11 11:41:03 +02:00
dd70b30f76 fix: don't use full pk fingerprint, only use first 8 chars 2020-09-11 11:41:03 +02:00
3e8e3c912b fix: don't doubly apply 822 texwrapper 2020-09-11 11:41:03 +02:00
5d0e3f36b4 fix: unhandled charset in header 2020-09-11 11:41:03 +02:00
da751a38e3 fix: public key names and content types 2020-09-11 11:41:03 +02:00
f9af17dd9b fix: allow unknown encodings during initial parse 2020-09-11 11:41:03 +02:00
f622ecf678 feat: logging throughout parser 2020-09-11 11:41:03 +02:00
475e673b87 feat: add logging for encoding detection 2020-09-11 11:41:03 +02:00
3916ddc8e4 fix: allow overriding sign via contact settings if set 2020-09-11 11:41:03 +02:00
ef2ace0afe fix: always check charset before utf8 validity 2020-09-11 11:41:03 +02:00
b5d3737a7e fix: sign not overriding global 2020-09-11 11:41:03 +02:00
d872d77cf5 fix: draft mime type instead of composermode 2020-09-11 11:41:03 +02:00
1f17628399 fix: unequal number of rich/plain parts 2020-09-11 11:41:03 +02:00
4ab8f7d6b5 fix: pubkey should not be collected as attachment 2020-09-11 11:41:03 +02:00
fa5f4acdac docs: add docstring for buildBodies 2020-09-11 11:41:03 +02:00
642666fa59 docs: add docstrings for walker/visitor handlers/rules 2020-09-11 11:41:03 +02:00
a2cf5374b9 feat: more efficient regexp use in parser 2020-09-11 11:41:03 +02:00
6a7a77fc51 refactor: tidier encoding detection 2020-09-11 11:41:03 +02:00
f4dfadce52 feat: attach public key 2020-09-11 11:41:03 +02:00
9ba08e5edb refactor: remove dead code 2020-09-11 11:41:03 +02:00
9821b5bbc2 feat: recreate message with parser's writer 2020-09-11 11:41:03 +02:00
5343a6fc0f fix: fallback to detecting charset if cannot handle specified one 2020-09-11 11:41:03 +02:00
180c6699e0 fix: don't select multipart/alternative if length is 0 2020-09-11 11:41:03 +02:00
7d1b0d0a40 docs: changelog 2020-09-11 11:41:03 +02:00
caff73d06c docs: add HELP about 7bit filter 2020-09-11 11:41:03 +02:00
f4d073b4cf test: ignore weird test for now 2020-09-11 11:41:02 +02:00
65d8b382d0 fix: panic when no params available 2020-09-11 11:41:02 +02:00
0e7e13211b refactor: don't reconstruct mimeBody 2020-09-11 11:41:02 +02:00
7e1af9ff4e fix: linter issues 2020-09-11 11:41:02 +02:00
37186846db feat: wrap attachment lines as per rfc822 2020-09-11 11:41:02 +02:00
a5a61c9428 feat: set attachment headers 2020-09-11 11:41:02 +02:00
ea01c155da feat: handle foreign encodings 2020-09-11 11:41:02 +02:00
f4374a02da refactor: tidy a bit 2020-09-11 11:41:02 +02:00
0d4d95360f feat: set header 2020-09-11 11:41:02 +02:00
f88071b2ca feat: parse date 2020-09-11 11:41:02 +02:00
e01a523ae3 feat: pull out most things as attachments 2020-09-11 11:41:02 +02:00
c6b18b45b5 feat: better handling of multipart messages 2020-09-11 11:41:02 +02:00
a7da66ccbc feat: enter and exit handlers 2020-09-11 11:41:02 +02:00
8bd74c5edc feat: set mime type 2020-09-11 11:41:02 +02:00
2b36d3ab7b feat: attach public key 2020-09-11 11:41:02 +02:00
45b863f931 feat: parse most header values 2020-09-11 11:41:02 +02:00
953150cfdb feat: add part getter 2020-09-11 11:41:02 +02:00
6ea3fc1963 feat: initial parser exposing walker/writer 2020-09-11 11:41:02 +02:00
7207a5d59e docs: changelog 2020-09-11 09:08:19 +00:00
dd2264da6f fix: notify of unencrypted recipient 2020-09-11 09:08:19 +00:00
9261b6337e docs: changelog 2020-09-11 10:48:27 +02:00
4f6e8c30c7 fix: use correct package type for signed inline 2020-09-11 10:29:02 +02:00
614a00eac1 Update release date for Congo in Changelog 2020-09-09 12:23:42 +02:00
de58c7a905 Cookies for Import-Export 2020-09-09 09:09:35 +02:00
2e439e17cf Remove unused scope methods 2020-09-09 06:21:02 +00:00
f73aeec97f Update changelog 2020-09-08 08:43:05 +00:00
8a7b4bb919 Improve user agent 2020-09-08 08:43:05 +00:00
78fd73ee2a Merge branch 'release/congo' into devel 2020-09-08 09:37:05 +02:00
33bf64cc4e Fix hover on links in popups 2020-09-04 10:43:59 +02:00
bb1d27a5be Do not ignore errors 2020-09-03 14:36:12 +02:00
9218598140 Update routes to API v4 2020-08-31 07:42:20 +00:00
af89931f05 Hardcoded version 2020-08-27 13:59:07 +02:00
84147a2cb0 Fix flaky tests 2020-08-25 10:20:49 +02:00
2269a9edb7 Pause event loop while FETCHing to prevetn EXPUNGE 2020-08-24 08:26:31 +00:00
273 changed files with 26821 additions and 3080 deletions

3
.gitignore vendored
View File

@ -29,6 +29,7 @@ frontend/qml/*.qmlc
# Build files # Build files
bridge_darwin_*.tgz bridge_darwin_*.tgz
cmd/Desktop-Bridge/deploy cmd/Desktop-Bridge/deploy
cmd/Import-Export/deploy
internal/frontend/qt*/moc.cpp internal/frontend/qt*/moc.cpp
internal/frontend/qt*/moc.go internal/frontend/qt*/moc.go
internal/frontend/qt*/moc.h internal/frontend/qt*/moc.h
@ -43,4 +44,4 @@ internal/frontend/rcc.qrc
internal/frontend/rcc_cgo_*.go internal/frontend/rcc_cgo_*.go
vendor-cache/ vendor-cache/
/main.go /main.go

View File

@ -82,7 +82,9 @@ dependency-updates:
script: script:
- make build - make build
artifacts: artifacts:
expire_in: 2 week # Note: The latest artifacts for refs are locked against deletion, and kept regardless of the expiry time.
# Introduced in GitLab 13.0 behind a disabled feature flag, and made the default behavior in GitLab 13.4.
expire_in: 1 day
build-linux: build-linux:
extends: .build-base extends: .build-base
@ -91,6 +93,17 @@ build-linux:
paths: paths:
- bridge_*.tgz - 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: build-ie-linux:
extends: .build-base extends: .build-base
script: script:
@ -124,6 +137,17 @@ build-darwin:
paths: paths:
- bridge_*.tgz - 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: build-ie-darwin:
extends: .build-darwin-base extends: .build-darwin-base
script: script:
@ -155,6 +179,23 @@ build-windows:
paths: paths:
- bridge_*.tgz - 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: build-ie-windows:
extends: .build-windows-base extends: .build-windows-base
script: script:

View File

@ -20,6 +20,9 @@ issues:
- gochecknoglobals - gochecknoglobals
- gochecknoinits - gochecknoinits
- gosec - gosec
- path: pkg/message/rfc5322
linters:
- dupl
linters-settings: linters-settings:
godox: godox:

View File

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

View File

@ -4,12 +4,137 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Unreleased ## Unreleased
## [IE 0.2.x] Congo ### 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.
## [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 ### Added
* GODT-633 Persistent anonymous API cookies for better load balancing and abuse detection. * GODT-633 Persistent anonymous API cookies for better load balancing and abuse detection.
* GODT-461 Add support for `\Deleted` flag.
### Changed ### 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-409 Set flags have to replace all flags.
* GODT-531 Better way to add trusted certificate in macOS. * GODT-531 Better way to add trusted certificate in macOS.
* Bumped golangci-lint to v1.29.0 * Bumped golangci-lint to v1.29.0
@ -36,6 +161,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* golang.org/x/text v0.3.2 -> v0.3.3 * golang.org/x/text v0.3.2 -> v0.3.3
* Set first-start to false in bridge, not in frontend. * Set first-start to false in bridge, not in frontend.
* GODT-400 Refactor sendingInfo. * GODT-400 Refactor sendingInfo.
* GODT-513 Update routes to API v4.
* GODT-551 Do not ignore errors during message flagging.
* GODT-380 Adding IE GUI to Bridge repo and building * GODT-380 Adding IE GUI to Bridge repo and building
* BR: extend functionality of PopupDialog * BR: extend functionality of PopupDialog
* BR: makefile APP_VERSION instead of BRIDGE_VERSION * BR: makefile APP_VERSION instead of BRIDGE_VERSION
@ -49,11 +176,14 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* IE: Removed `onLoginFinished` * IE: Removed `onLoginFinished`
* Structure for transfer rules in QML * Structure for transfer rules in QML
* GODT-213 Convert panics from message parser to error. * GODT-213 Convert panics from message parser to error.
* GODT-585 Do not allow deleting messages from All Mail.
### Fixed ### Fixed
* GODT-655 Fix date picker with automatic Windows DST * GODT-655 Fix date picker with automatic Windows DST
* GODT-454 Fix send on closed channel when receiving unencrypted send confirmation from GUI. * GODT-454 Fix send on closed channel when receiving unencrypted send confirmation from GUI.
* GODT-597 Duplicate sending when draft creation takes too long * GODT-597 Duplicate sending when draft creation takes too long
* GODT-634 Hover on links in popups.
## [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) ## [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)

View File

@ -9,8 +9,9 @@ TARGET_OS?=${GOOS}
## Build ## Build
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go .PHONY: build build-ie build-nogui build-ie-nogui check-has-go
BRIDGE_APP_VERSION?=1.4.0-git # Keep version hardcoded so app build works also without Git repository.
IE_APP_VERSION?=1.0.0-git BRIDGE_APP_VERSION?=1.5.0-git
IE_APP_VERSION?=1.2.0-git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns SRC_ICNS:=Bridge.icns
@ -56,7 +57,6 @@ ifeq "${TARGET_CMD}" "Import-Export"
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
endif endif
build: ${TGZ_TARGET} build: ${TGZ_TARGET}
build-ie: build-ie:
TARGET_CMD=Import-Export $(MAKE) build TARGET_CMD=Import-Export $(MAKE) build
@ -264,7 +264,6 @@ run-ie-qt:
run-ie-nogui: run-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) run-nogui TARGET_CMD=Import-Export $(MAKE) run-nogui
clean-frontend-qt: clean-frontend-qt:
$(MAKE) -C internal/frontend/qt -f Makefile.local clean $(MAKE) -C internal/frontend/qt -f Makefile.local clean
clean-frontend-qt-ie: clean-frontend-qt-ie:
@ -281,3 +280,8 @@ clean: clean-vendor
rm -rf cmd/Import-Export/deploy rm -rf cmd/Import-Export/deploy
rm -f build last.log mem.pprof main.go rm -f build last.log mem.pprof main.go
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso
.PHONY: generate
generate:
go generate ./...
$(MAKE) add-license

View File

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

View File

@ -21,9 +21,11 @@ import (
"runtime/pprof" "runtime/pprof"
"github.com/ProtonMail/proton-bridge/internal/cmd" "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/events"
"github.com/ProtonMail/proton-bridge/internal/frontend" "github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/importexport" "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/updates"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
@ -36,6 +38,10 @@ import (
) )
const ( 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" appName = "importExport"
appNameDash = "import-export-app" appNameDash = "import-export-app"
) )
@ -58,7 +64,7 @@ func main() {
// IMPORTANT: ***Read the comments before CHANGING the order *** // IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen] func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ... // We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(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 // We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash // not dependent on anything else. If that fails, it tries to create crash
@ -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. // implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener)) 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) importexportInstance := importexport.New(cfg, panicHandler, eventListener, cm, credentialsStore)
// Decide about frontend mode before initializing rest of import-export. // Decide about frontend mode before initializing rest of import-export.

18
go.mod
View File

@ -13,16 +13,18 @@ require (
require ( require (
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
github.com/Masterminds/semver/v3 v3.1.0
github.com/ProtonMail/go-appdir v1.1.0 github.com/ProtonMail/go-appdir v1.1.0
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp/v2 v2.0.1 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/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
github.com/andybalholm/cascadia v1.2.0 github.com/antlr/antlr4 v0.0.0-20201020194047-0a7eaede42b0
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
@ -34,8 +36,8 @@ require (
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
github.com/emersion/go-mbox v1.0.0 github.com/emersion/go-mbox v1.0.2
github.com/emersion/go-message v0.11.1 github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
@ -46,11 +48,9 @@ require (
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1 github.com/google/go-cmp v0.5.1
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/go-delve/delve v1.4.1 // indirect
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-multierror v1.1.0
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 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/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
@ -60,13 +60,11 @@ require (
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/psampaz/go-mod-outdated v0.6.0 // indirect
github.com/sirupsen/logrus v1.6.0 github.com/sirupsen/logrus v1.6.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-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/twinj/uuid v1.0.0 // indirect
github.com/urfave/cli v1.22.4 github.com/urfave/cli v1.22.4
go.etcd.io/bbolt v1.3.5 go.etcd.io/bbolt v1.3.5
@ -77,8 +75,8 @@ require (
replace ( replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-imap => github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201016095853-a7520cc904d3
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8
) )

102
go.sum
View File

@ -1,10 +1,12 @@
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU= github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8 h1:u1j0xLTrCHpNS40B6m4Sv3IVUz5m9jt+AnTIopT3IgM=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig= github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
@ -13,6 +15,8 @@ github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg= github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-imap v0.0.0-20201016095853-a7520cc904d3 h1:Jvv9t3rSg/ID3Fh+uYsxgmvNI9fYnlab4vtBsbPtmq8=
github.com/ProtonMail/go-imap v0.0.0-20201016095853-a7520cc904d3/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
@ -23,25 +27,24 @@ github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GU
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU= github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY= github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA= github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U= github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/antlr/antlr4 v0.0.0-20201020194047-0a7eaede42b0 h1:7RW94Pqb4Twsfpz42ALQ+sD0cUUpN8HF4uzKyQf2D8Y=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/antlr/antlr4 v0.0.0-20201020194047-0a7eaede42b0/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA= 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/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -64,11 +67,11 @@ github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 h1:z8T
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4= github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.0 h1:HN6aKbyqmgIfK9fS/gen+NRr2wXLSxZXWfdAIAnzQPc= github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
github.com/emersion/go-mbox v1.0.0/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI= github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-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-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
@ -84,25 +87,12 @@ github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JY
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
github.com/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/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/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-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -110,24 +100,14 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
github.com/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/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/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/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/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg= github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY= github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE= github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/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/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -137,61 +117,41 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/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/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A= github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
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.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo= github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/olekukonko/tablewriter v0.0.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 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/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 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/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/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/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/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -201,40 +161,25 @@ 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt 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 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@ -246,41 +191,28 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/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 h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-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/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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/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-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Fri 04 Sep 2020 01:57:36 PM CEST. DO NOT EDIT. // Code generated by ./credits.sh at Wed Nov 4 12:24:36 PM CET 2020. DO NOT EDIT.
package bridge 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/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/antlr/antlr4;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/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/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;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;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -15,17 +15,17 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Fri 04 Sep 2020 01:57:36 PM CEST'. DO NOT EDIT. // Code generated by ./release-notes.sh at 'Wed Nov 4 12:24:35 PM CET 2020'. DO NOT EDIT.
package bridge 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. const ReleaseNotes = `Ensured better message flow by refactoring both address and date parsing
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. Improved secure connectivity checks
User interaction improvements: Some smaller improvements in specific cases to make the interaction with Proton Bridge clearer for the user Better deb packaging
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. More robust error handling
• General stability improvements: Improvements to the behavior of the application under various unstable internet conditions.
` `
const ReleaseFixedBugs = `Fixed a slew of smaller bugs and some conditions which could cause the application to crash. const ReleaseFixedBugs = `Ensured that conversations are properly threaded
The full changelog can be found at https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md Fixed Linux font issues (Fedora)
• Better handling of Mime encrypted messages
` `

View File

@ -79,7 +79,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return return
} }
_, err = client.Auth2FA(twoFactor, auth) err = client.Auth2FA(twoFactor, auth)
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return

View File

@ -126,7 +126,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return return
} }
_, err = client.Auth2FA(twoFactor, auth) err = client.Auth2FA(twoFactor, auth)
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return

View File

@ -354,7 +354,7 @@ Window {
} else { } else {
return qsTr('A new version of Bridge is available.<br> 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> 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>', <a href="%3">%3</a>',
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage) "Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
} }

View File

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

View File

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

View File

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

View File

@ -279,9 +279,8 @@ Dialog {
titleTo : root.address titleTo : root.address
} }
Rectangle { Column {
id: masterImportSettings id: masterImportSettings
height: 150 // fixme
anchors { anchors {
right : parent.right right : parent.right
left : parent.left left : parent.left
@ -291,45 +290,47 @@ Dialog {
rightMargin : Style.main.leftMargin rightMargin : Style.main.leftMargin
bottomMargin : Style.main.bottomMargin bottomMargin : Style.main.bottomMargin
} }
color: Style.dialog.background
Text { spacing: Style.main.bottomMargin
id: labelMasterImportSettings
text: qsTr("Master import settings:")
font { Row {
bold: true spacing: masterImportSettings.width - labelMasterImportSettings.width - resetSourceButton.width
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
color: Style.main.text
InfoToolTip { Text {
info: qsTr( id: labelMasterImportSettings
"If master import date range is selected only emails within this range will be imported, unless it is specified differently in folder date range.", text: qsTr("Master import settings:")
"Text in master import settings tooltip."
) font {
anchors { bold: true
left: parent.right family: Style.fontawesome.name
bottom: parent.bottom pointSize: Style.main.fontSize * Style.pt
leftMargin : Style.dialog.leftMargin }
color: Style.main.text
InfoToolTip {
anchors {
left: parent.right
bottom: parent.bottom
leftMargin : Style.dialog.leftMargin
}
info: qsTr(
"If master import date range is selected only emails within this range will be imported, unless it is specified differently in folder date range.",
"Text in master import settings tooltip."
)
} }
} }
}
// Reset all to default // Reset all to default
ClickIconText { ClickIconText {
anchors { id: resetSourceButton
right: parent.right text:qsTr("Reset all settings to default")
bottom: labelMasterImportSettings.bottom iconText: Style.fa.refresh
} textColor: Style.main.textBlue
text:qsTr("Reset all settings to default") onClicked: {
iconText: Style.fa.refresh go.resetSource()
textColor: Style.main.textBlue root.decrementCurrentIndex()
onClicked: { timer.start()
go.resetSource() }
root.decrementCurrentIndex()
timer.start()
} }
} }
@ -348,49 +349,40 @@ Dialog {
InlineDateRange { InlineDateRange {
id: globalDateRange id: globalDateRange
anchors {
left : parent.left
top : line.bottom
topMargin : Style.dialog.topMargin
}
} }
// Add global label (inline) // Add global label (inline)
InlineLabelSelect { InlineLabelSelect {
id: globalLabels id: globalLabels
anchors { }
left : parent.left }
top : globalDateRange.bottom
topMargin : Style.dialog.topMargin // Buttons
} Row {
//labelWidth : globalDateRange.labelWidth spacing: Style.dialog.spacing
anchors {
right: parent.right
bottom: parent.bottom
rightMargin: Style.main.leftMargin
bottomMargin: Style.main.bottomMargin
} }
// Buttons ButtonRounded {
Row { id: buttonCancelThree
spacing: Style.dialog.spacing fa_icon : Style.fa.times
anchors{ text : qsTr("Cancel", "todo")
bottom : parent.bottom color_main : Style.dialog.textBlue
right : parent.right onClicked : root.cancel()
} }
ButtonRounded { ButtonRounded {
id: buttonCancelThree id: buttonNextThree
fa_icon : Style.fa.times fa_icon : Style.fa.check
text : qsTr("Cancel", "todo") text : qsTr("Import", "todo")
color_main : Style.dialog.textBlue color_main : Style.dialog.background
onClicked : root.cancel() color_minor : Style.dialog.textBlue
} isOpaque : true
onClicked : root.okay()
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 +475,30 @@ Dialog {
} }
} }
Text { Row {
property int fails: go.progressFails property int fails: go.progressFails
visible: fails > 0 visible: fails > 0
color : Style.main.textRed
font.family: Style.fontawesome.name
font.pointSize: Style.main.fontSize * Style.pt
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: Style.fa.exclamation_circle + " " + (
fails == 1 ? Text {
qsTr("%1 message failed to be imported").arg(fails) : color: Style.main.textRed
qsTr("%1 messages failed to be imported").arg(fails) font {
) pointSize : Style.dialog.fontSize * Style.pt
family : Style.fontawesome.name
}
text: Style.fa.exclamation_circle
}
Text {
property int fails: go.progressFails
color: Style.main.textRed
font.pointSize: Style.main.fontSize * Style.pt
text: " " + (
fails == 1 ?
qsTr("%1 message failed to be imported").arg(fails) :
qsTr("%1 messages failed to be imported").arg(fails)
)
}
} }
Row { // buttons Row { // buttons
@ -575,12 +579,23 @@ Dialog {
anchors.centerIn : finalReport anchors.centerIn : finalReport
spacing : Style.dialog.heightSeparator spacing : Style.dialog.heightSeparator
Text { Row {
text: go.progressDescription!="" ? qsTr("Import failed: %1").arg(go.progressDescription) : Style.fa.check_circle + " " + qsTr("Import completed successfully")
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen
font.bold : true Text {
font.family: Style.fontawesome.name font {
pointSize: Style.dialog.fontSize * Style.pt
family: Style.fontawesome.name
}
color: Style.main.textGreen
text: go.progressDescription!="" ? "" : Style.fa.check_circle
}
Text {
text: go.progressDescription!="" ? qsTr("Import failed: %1").arg(go.progressDescription) : " " + qsTr("Import completed successfully")
color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen
font.bold : true
}
} }
Text { Text {
@ -773,11 +788,6 @@ Dialog {
errorPopup.hide() errorPopup.hide()
} }
onClickedNo : { onClickedNo : {
if (errorPopup.msgID == "ask_send_report") {
errorPopup.hide()
return
}
go.resumeProcess()
errorPopup.hide() errorPopup.hide()
} }

View File

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

View File

@ -274,15 +274,15 @@ Window {
} }
} else { } else {
if (go.goos=="linux") { 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> 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>', <a href="%4">%4</a>',
"Message for update in Linux").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage) "Message for update in Linux").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
} else { } 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> 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>', <a href="%4">%4</a>',
"Message for update in Win/Mac").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage) "Message for update in Win/Mac").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
} }

View File

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

View File

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

View File

@ -106,6 +106,7 @@ Rectangle {
} }
MouseArea { MouseArea {
anchors.fill: mainText anchors.fill: mainText
cursorShape: mainText.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
} }

View File

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

View File

@ -208,7 +208,7 @@ func (a *Accounts) Auth2FA(twoFacAuth string) int {
if a.auth == nil || a.authClient == nil { if a.auth == nil || a.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient) err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient)
} else { } else {
_, err = a.authClient.Auth2FA(twoFacAuth, a.auth) err = a.authClient.Auth2FA(twoFacAuth, a.auth)
} }
if a.showLoginError(err, "auth2FA") { if a.showLoginError(err, "auth2FA") {

View File

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

View File

@ -338,9 +338,7 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
break break
} }
failed, imported, _, _, total := progress.GetCounts() failed, imported, _, _, total := progress.GetCounts()
if total != 0 { f.Qml.SetTotal(int(total))
f.Qml.SetTotal(int(total))
}
f.Qml.SetProgressFails(int(failed)) f.Qml.SetProgressFails(int(failed))
f.Qml.SetProgressDescription(progress.PauseReason()) f.Qml.SetProgressDescription(progress.PauseReason())
if total > 0 { if total > 0 {
@ -351,6 +349,9 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
} }
} }
} }
// Counts will add lost messages only once the progress is completeled.
failed, _, _, _, _ := progress.GetCounts()
f.Qml.SetProgressFails(int(failed))
if err := progress.GetFatalError(); err != nil { if err := progress.GetFatalError(); err != nil {
f.Qml.SetProgressDescription(err.Error()) f.Qml.SetProgressDescription(err.Error())

View File

@ -164,7 +164,7 @@ func (s *FrontendQt) auth2FA(twoFacAuth string) int {
if s.auth == nil || s.authClient == nil { if s.auth == nil || s.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient) err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient)
} else { } else {
_, err = s.authClient.Auth2FA(twoFacAuth, s.auth) err = s.authClient.Auth2FA(twoFacAuth, s.auth)
} }
if s.showLoginError(err, "auth2FA") { if s.showLoginError(err, "auth2FA") {

View File

@ -23,7 +23,6 @@ import (
"sync" "sync"
"time" "time"
imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
@ -45,9 +44,6 @@ type imapBackend struct {
users map[string]*imapUser users map[string]*imapUser
usersLocker sync.Locker usersLocker sync.Locker
lastMailClient imapid.ID
lastMailClientLocker sync.Locker
imapCache map[string]map[string]string imapCache map[string]map[string]string
imapCachePath string imapCachePath string
imapCacheLock *sync.RWMutex imapCacheLock *sync.RWMutex
@ -87,9 +83,6 @@ func newIMAPBackend(
users: map[string]*imapUser{}, users: map[string]*imapUser{},
usersLocker: &sync.Mutex{}, usersLocker: &sync.Mutex{},
lastMailClient: imapid.ID{imapid.FieldName: clientNone},
lastMailClientLocker: &sync.Mutex{},
imapCachePath: cfg.GetIMAPCachePath(), imapCachePath: cfg.GetIMAPCachePath(),
imapCacheLock: &sync.RWMutex{}, imapCacheLock: &sync.RWMutex{},
} }
@ -164,7 +157,9 @@ func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMA
if err := imapUser.user.CheckBridgeLogin(password); err != nil { if err := imapUser.user.CheckBridgeLogin(password); err != nil {
log.WithError(err).Error("Could not check bridge password") 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. // 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 // It's therefore good to have a timeout after a bad login so that we can slow
// those requests down a little bit. // those requests down a little bit.
@ -192,23 +187,6 @@ func (ib *imapBackend) CreateMessageLimit() *uint32 {
return nil 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. // monitorDisconnectedUsers removes users when it receives a close connection event for them.
func (ib *imapBackend) monitorDisconnectedUsers() { func (ib *imapBackend) monitorDisconnectedUsers() {
ch := make(chan string) ch := make(chan string)

View File

@ -80,7 +80,7 @@ func (ib *imapBackend) removeFromCache(userID, label, toRemove string) {
func (ib *imapBackend) getCacheList(userID, label string) (list string) { func (ib *imapBackend) getCacheList(userID, label string) (list string) {
if err := ib.loadIMAPCache(); err != nil { if err := ib.loadIMAPCache(); err != nil {
log.Warn("Could not load cache: ", err) log.WithError(err).Warn("Could not load cache")
} }
ib.imapCacheLock.Lock() ib.imapCacheLock.Lock()
@ -97,7 +97,9 @@ func (ib *imapBackend) getCacheList(userID, label string) (list string) {
ib.imapCacheLock.Unlock() ib.imapCacheLock.Unlock()
_ = ib.saveIMAPCache() if err := ib.saveIMAPCache(); err != nil {
log.WithError(err).Warn("Could not save cache")
}
return return
} }

View File

@ -76,5 +76,9 @@ func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
} }
func (u *bridgeUserWrap) GetStore() storeUserProvider { func (u *bridgeUserWrap) GetStore() storeUserProvider {
return newStoreUserWrap(u.User.GetStore()) store := u.User.GetStore()
if store == nil {
return nil
}
return newStoreUserWrap(store)
} }

View File

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

View File

@ -22,12 +22,6 @@ import "github.com/sirupsen/logrus"
const ( const (
fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP). fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP).
fetchAttachmentsWorkers = 5 // In how many workers to fetch attachments (for one message). 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 ( var (

View File

@ -80,7 +80,10 @@ func (im *imapMailbox) Info() (*imap.MailboxInfo, error) {
} }
func (im *imapMailbox) getFlags() []string { func (im *imapMailbox) getFlags() []string {
flags := []string{imap.NoInferiorsAttr} // Subfolders are not yet supported by API. flags := []string{}
if !im.storeMailbox.IsFolder() || im.storeMailbox.IsSystem() {
flags = append(flags, imap.NoInferiorsAttr) // Subfolders are not supported for System or Label
}
switch im.storeMailbox.LabelID() { switch im.storeMailbox.LabelID() {
case pmapi.SentLabel: case pmapi.SentLabel:
flags = append(flags, specialuse.Sent) flags = append(flags, specialuse.Sent)
@ -173,9 +176,8 @@ func (im *imapMailbox) Check() error {
// Expunge permanently removes all messages that have the \Deleted flag set // Expunge permanently removes all messages that have the \Deleted flag set
// from the currently selected mailbox. // from the currently selected mailbox.
// Our messages do not have \Deleted flag, nothing to do here.
func (im *imapMailbox) Expunge() error { func (im *imapMailbox) Expunge() error {
return nil return im.storeMailbox.RemoveDeleted()
} }
func (im *imapMailbox) ListQuotas() ([]string, error) { func (im *imapMailbox) ListQuotas() ([]string, error) {

View File

@ -24,7 +24,6 @@ import (
"mime/multipart" "mime/multipart"
"net/mail" "net/mail"
"net/textproto" "net/textproto"
"regexp"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -37,7 +36,6 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
enmime "github.com/jhillyerd/enmime"
"github.com/pkg/errors" "github.com/pkg/errors"
openpgperrors "golang.org/x/crypto/openpgp/errors" openpgperrors "golang.org/x/crypto/openpgp/errors"
) )
@ -142,18 +140,19 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
references := m.Header.Get("References") references := m.Header.Get("References")
referenceList := strings.Fields(references) referenceList := strings.Fields(references)
if len(referenceList) > 0 { // In case there is a mail client which corrupts headers, try
// "References" too.
if internalID == "" && len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1] lastReference := referenceList[len(referenceList)-1]
// In case we are using a mail client which corrupts headers, try "References" too. match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference)
re := regexp.MustCompile(pmapi.InternalReferenceFormat) if len(match) == 2 {
match := re.FindStringSubmatch(lastReference) internalID = match[1]
if len(match) > 0 {
internalID = match[0]
} }
} }
// Avoid appending a message which is already on the server. Apply the new // Avoid appending a message which is already on the server. Apply the
// label instead. This sometimes happens which Outlook (it uses APPEND instead of COPY). // new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY).
if internalID != "" { if internalID != "" {
// Check to see if this belongs to a different address in split mode or another ProtonMail account. // Check to see if this belongs to a different address in split mode or another ProtonMail account.
msg, err := im.storeMailbox.GetMessage(internalID) msg, err := im.storeMailbox.GetMessage(internalID)
@ -221,6 +220,9 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
} }
case imap.FetchFlags: case imap.FetchFlags:
msg.Flags = message.GetFlags(m) msg.Flags = message.GetFlags(m)
if storeMessage.IsMarkedDeleted() {
msg.Flags = append(msg.Flags, imap.DeletedFlag)
}
case imap.FetchInternalDate: case imap.FetchInternalDate:
msg.InternalDate = time.Unix(m.Time, 0) msg.InternalDate = time.Unix(m.Time, 0)
case imap.FetchRFC822Size: case imap.FetchRFC822Size:
@ -238,26 +240,30 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
return nil, err return nil, err
} }
default: default:
s := item if err = im.getLiteralForSection(item, msg, storeMessage); err != nil {
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 {
return return
} }
msg.Body[section] = literal
} }
} }
return msg, err 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) ( func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (
structure *message.BodyStructure, structure *message.BodyStructure,
bodyReader *bytes.Reader, err error, bodyReader *bytes.Reader, err error,
@ -446,17 +452,6 @@ func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err erro
return 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) { func (im *imapMailbox) writeAttachmentBody(w io.Writer, m *pmapi.Message, att *pmapi.Attachment) (err error) {
// Retrieve encrypted attachment. // Retrieve encrypted attachment.
r, err := im.user.client().GetAttachment(att.ID) r, err := im.user.client().GetAttachment(att.ID)

View File

@ -57,7 +57,11 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
return im.addOrRemoveFlags(operation, messageIDs, flags) 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 seen := false
flagged := false flagged := false
deleted := false deleted := false
@ -77,29 +81,48 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error {
} }
if seen { if seen {
_ = im.storeMailbox.MarkMessagesRead(messageIDs) if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
return err
}
} else { } else {
_ = im.storeMailbox.MarkMessagesUnread(messageIDs) if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil {
return err
}
} }
if flagged { if flagged {
_ = im.storeMailbox.MarkMessagesStarred(messageIDs) if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
return err
}
} else { } else {
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs) if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil {
return err
}
} }
if deleted { 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") // Spam should not be taken into action here as Outlook is using FLAGS
if err != nil { // without preserving junk flag. Probably it's because junk is not standard
return err // in the rfc3501 and thus Outlook expects calling FLAGS \Seen will not
} // change the state of junk or other non-standard flags.
// Still, its safe to label as spam once any client sends the request.
if spam { if spam {
_ = spamMailbox.LabelMessages(messageIDs) spamMailbox, err := im.storeAddress.GetMailbox("Spam")
} else { if err != nil {
_ = spamMailbox.UnlabelMessages(messageIDs) return err
}
if err := spamMailbox.LabelMessages(messageIDs); err != nil {
return err
}
} }
return nil return nil
@ -111,22 +134,36 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
case imap.SeenFlag: case imap.SeenFlag:
switch operation { switch operation {
case imap.AddFlags: case imap.AddFlags:
_ = im.storeMailbox.MarkMessagesRead(messageIDs) if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
return err
}
case imap.RemoveFlags: case imap.RemoveFlags:
_ = im.storeMailbox.MarkMessagesUnread(messageIDs) if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil {
return err
}
} }
case imap.FlaggedFlag: case imap.FlaggedFlag:
switch operation { switch operation {
case imap.AddFlags: case imap.AddFlags:
_ = im.storeMailbox.MarkMessagesStarred(messageIDs) if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
return err
}
case imap.RemoveFlags: case imap.RemoveFlags:
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs) if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil {
return err
}
} }
case imap.DeletedFlag: case imap.DeletedFlag:
if operation == imap.RemoveFlags { switch operation {
break // Nothing to do, no message has the \Deleted flag. 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: case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag:
// Not supported. // Not supported.
case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag: case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag:
@ -140,9 +177,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 // No label removal is necessary because Spam and Inbox are both exclusive labels so the backend
// will automatically take care of label removal. // will automatically take care of label removal.
case imap.AddFlags: case imap.AddFlags:
_ = storeMailbox.LabelMessages(messageIDs) if err := storeMailbox.LabelMessages(messageIDs); err != nil {
return err
}
case imap.RemoveFlags: case imap.RemoveFlags:
_ = storeMailbox.UnlabelMessages(messageIDs) if err := storeMailbox.UnlabelMessages(messageIDs); err != nil {
return err
}
} }
} }
} }
@ -186,6 +227,20 @@ func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel
return err 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 // 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. // it, it will be removed completely and we cannot label it back.
if err := targetStoreMailbox.LabelMessages(messageIDs); err != nil { if err := targetStoreMailbox.LabelMessages(messageIDs); err != nil {
@ -197,6 +252,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) targetSeqSet := targetStoreMailbox.GetUIDList(messageIDs)
return uidplus.CopyResponse(targetStoreMailbox.UIDValidity(), sourceSeqSet, targetSeqSet) return uidplus.CopyResponse(targetStoreMailbox.UIDValidity(), sourceSeqSet, targetSeqSet)
} }
@ -321,6 +383,9 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
if !m.Has(pmapi.FlagOpened) { if !m.Has(pmapi.FlagOpened) {
messageFlagsMap[imap.RecentFlag] = true messageFlagsMap[imap.RecentFlag] = true
} }
if storeMessage.IsMarkedDeleted() {
messageFlagsMap[imap.DeletedFlag] = true
}
flagMatch := true flagMatch := true
for _, flag := range criteria.WithFlags { for _, flag := range criteria.WithFlags {
@ -383,6 +448,12 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
im.panicHandler.HandlePanic() 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 var markAsReadIDs []string
markAsReadMutex := &sync.Mutex{} markAsReadMutex := &sync.Mutex{}

View File

@ -28,6 +28,7 @@ import (
imapid "github.com/ProtonMail/go-imap-id" imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "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/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
@ -60,34 +61,12 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
s.UpgradeError = imapBackend.upgradeError s.UpgradeError = imapBackend.upgradeError
serverID := imapid.ID{ serverID := imapid.ID{
imapid.FieldName: "ProtonMail", imapid.FieldName: "ProtonMail Bridge",
imapid.FieldVendor: "Proton Technologies AG", imapid.FieldVendor: "Proton Technologies AG",
imapid.FieldSupportURL: "https://protonmail.com/support", imapid.FieldSupportURL: "https://protonmail.com/support",
} }
s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server { 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 { return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password) user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil { if err != nil {
@ -105,7 +84,7 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
imapidle.NewExtension(), imapidle.NewExtension(),
imapmove.NewExtension(), imapmove.NewExtension(),
imapspecialuse.NewExtension(), imapspecialuse.NewExtension(),
imapid.NewExtension(serverID), id.NewExtension(serverID, imapBackend.bridge),
imapquota.NewExtension(), imapquota.NewExtension(),
imapappendlimit.NewExtension(), imapappendlimit.NewExtension(),
imapunselect.NewExtension(), imapunselect.NewExtension(),

View File

@ -41,6 +41,8 @@ type storeUserProvider interface {
attachedPublicKey, attachedPublicKey,
attachedPublicKeyName string, attachedPublicKeyName string,
parentID string) (*pmapi.Message, []*pmapi.Attachment, error) parentID string) (*pmapi.Message, []*pmapi.Attachment, error)
PauseEventLoop(bool)
} }
type storeAddressProvider interface { type storeAddressProvider interface {
@ -68,6 +70,7 @@ type storeMailboxProvider interface {
GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error) GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error)
GetLatestAPIID() (string, error) GetLatestAPIID() (string, error)
GetNextUID() (uint32, error) GetNextUID() (uint32, error)
GetDeletedAPIIDs() ([]string, error)
GetCounts() (dbTotal, dbUnread, dbUnreadSeqNum uint, err error) GetCounts() (dbTotal, dbUnread, dbUnreadSeqNum uint, err error)
GetUIDList(apiIDs []string) *uidplus.OrderedSeq GetUIDList(apiIDs []string) *uidplus.OrderedSeq
GetUIDByHeader(header *mail.Header) uint32 GetUIDByHeader(header *mail.Header) uint32
@ -81,8 +84,10 @@ type storeMailboxProvider interface {
MarkMessagesUnread(apiID []string) error MarkMessagesUnread(apiID []string) error
MarkMessagesStarred(apiID []string) error MarkMessagesStarred(apiID []string) error
MarkMessagesUnstarred(apiID []string) error MarkMessagesUnstarred(apiID []string) error
MarkMessagesDeleted(apiID []string) error
MarkMessagesUndeleted(apiID []string) error
ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error
DeleteMessages(apiID []string) error RemoveDeleted() error
} }
type storeMessageProvider interface { type storeMessageProvider interface {
@ -90,6 +95,7 @@ type storeMessageProvider interface {
UID() (uint32, error) UID() (uint32, error)
SequenceNumber() (uint32, error) SequenceNumber() (uint32, error)
Message() *pmapi.Message Message() *pmapi.Message
IsMarkedDeleted() bool
SetSize(int64) error SetSize(int64) error
SetContentTypeAndHeader(string, mail.Header) error SetContentTypeAndHeader(string, mail.Header) error

View File

@ -25,6 +25,7 @@
package uidplus package uidplus
import ( import (
"errors"
"fmt" "fmt"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
@ -113,18 +114,43 @@ func (os *OrderedSeq) String() string {
return out return out
} }
// UIDExpunge implements server.Handler but has no effect because Bridge is not // UIDExpunge implements server.Handler but Bridge is not supporting
// using EXPUNGE at all. The message is deleted right after it was flagged as // UID EXPUNGE with specific UIDs.
// \Deleted Bridge should simply ignore this command with empty `OK` response. type UIDExpunge struct {
// expunge *server.Expunge
// If not implemented it would cause harmless IMAP error. }
//
// This overrides the standard EXPUNGE functionality.
type UIDExpunge struct{}
func (e *UIDExpunge) Parse(fields []interface{}) error { log.Traceln("parse", fields); return nil } func newUIDExpunge() *UIDExpunge {
func (e *UIDExpunge) Handle(conn server.Conn) error { log.Traceln("handle"); return nil } return &UIDExpunge{expunge: &server.Expunge{}}
func (e *UIDExpunge) UidHandle(conn server.Conn) error { log.Traceln("uid handle"); return nil } //nolint[golint] }
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{} type extension struct{}
@ -143,7 +169,7 @@ func (ext *extension) Capabilities(c server.Conn) []string {
func (ext *extension) Command(name string) server.HandlerFactory { func (ext *extension) Command(name string) server.HandlerFactory {
if name == "EXPUNGE" { if name == "EXPUNGE" {
return func() server.Handler { return func() server.Handler {
return &UIDExpunge{} return newUIDExpunge()
} }
} }

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Fri 04 Sep 2020 01:57:36 PM CEST. DO NOT EDIT. // Code generated by ./credits.sh at Wed Nov 4 12:24:36 PM CET 2020. DO NOT EDIT.
package importexport 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/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/antlr/antlr4;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/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/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;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;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -15,19 +15,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Fri 04 Sep 2020 01:57:36 PM CEST'. DO NOT EDIT. // Code generated by ./release-notes.sh at 'Wed Nov 4 12:24:35 PM CET 2020'. DO NOT EDIT.
package importexport 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*** const ReleaseNotes = `• Improvements to the import from large mbox files with multiple labels
• Not allow to run multiple instances of the app or transfers at the same time
Complete code refactor in preparation of stable and open source release of the Import-Export app Various enhancements of the import process related to parsing
Increased number of supported mail providers by changing the way the folder structures are handled (NIL hierarchy delimiter) Cosmetic GUI changes
Improved handling for unstable internet and pause and resume behavior Better error handling
` `
const ReleaseFixedBugs = `Fixed rare cases where the application freezes when starting/stopping imports const ReleaseFixedBugs = `Linux font issues - Fedora specific
• Allowed current date to be included in the selected date range for both import and export • App response to the user pausing and canceling import or export
Improved manual update process Handling errors during update
• Limit space usage by on device application logs
` `

View File

@ -95,9 +95,8 @@ func (b *sendPreferencesBuilder) shouldEncrypt() bool {
return false return false
} }
func (b *sendPreferencesBuilder) withSign() { func (b *sendPreferencesBuilder) withSign(sign bool) {
v := true b.sign = &sign
b.sign = &v
} }
func (b *sendPreferencesBuilder) withSignDefault() { func (b *sendPreferencesBuilder) withSignDefault() {
@ -192,7 +191,7 @@ func (b *sendPreferencesBuilder) build() (p SendPreferences) {
p.Scheme = pmapi.PGPMIMEPackage p.Scheme = pmapi.PGPMIMEPackage
} }
case b.shouldSign() && !b.shouldEncrypt(): case b.shouldSign() && !b.shouldEncrypt() && b.getScheme() == pgpMIME:
p.Scheme = pmapi.ClearMIMEPackage p.Scheme = pmapi.ClearMIMEPackage
default: default:
@ -258,7 +257,7 @@ func (b *sendPreferencesBuilder) setInternalPGPSettings(
// We always encrypt and sign internal mail. // We always encrypt and sign internal mail.
b.withEncrypt(true) b.withEncrypt(true)
b.withSign() b.withSign(true)
// We use a custom scheme for internal messages. // We use a custom scheme for internal messages.
b.withScheme(pmInternal) b.withScheme(pmInternal)
@ -369,7 +368,7 @@ func (b *sendPreferencesBuilder) setExternalPGPSettingsWithWKDKeys(
// We always encrypt and sign external mail if WKD keys are present. // We always encrypt and sign external mail if WKD keys are present.
b.withEncrypt(true) b.withEncrypt(true)
b.withSign() b.withSign(true)
// If the contact has a specific Scheme preference, we set it (otherwise we // 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). // leave it unset to allow it to be filled in with the default value later).
@ -402,9 +401,13 @@ func (b *sendPreferencesBuilder) setExternalPGPSettingsWithoutWKDKeys(
) (err error) { ) (err error) {
b.withEncrypt(vCardData.Encrypt) b.withEncrypt(vCardData.Encrypt)
if vCardData.SignIsSet {
b.withSign(vCardData.Sign)
}
// Sign must be enabled whenever encrypt is. // Sign must be enabled whenever encrypt is.
if vCardData.Sign || vCardData.Encrypt { if vCardData.Encrypt {
b.withSign() b.withSign(true)
} }
// If the contact has a specific Scheme preference, we set it (otherwise we // If the contact has a specific Scheme preference, we set it (otherwise we
@ -475,7 +478,7 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
} }
if b.shouldEncrypt() { if b.shouldEncrypt() {
b.withSign() b.withSign(true)
} }
// If undefined, default to the user mail setting "Default PGP scheme". // If undefined, default to the user mail setting "Default PGP scheme".
@ -495,23 +498,11 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
if b.shouldSign() && b.getScheme() == pgpInline { if b.shouldSign() && b.getScheme() == pgpInline {
b.withMIMEType("text/plain") b.withMIMEType("text/plain")
} else { } else {
switch mailSettings.ComposerMode { b.withMIMETypeDefault(mailSettings.DraftMIMEType)
case pmapi.ComposerModeNormal:
b.withMIMETypeDefault("text/html")
case pmapi.ComposerModePlain:
b.withMIMETypeDefault("text/plain")
}
} }
} }
func (b *sendPreferencesBuilder) setMIMEPreferences(composerMIMEType string) { func (b *sendPreferencesBuilder) setMIMEPreferences(composerMIMEType string) {
// If the sign flag (that we just determined above) is true we use the scheme
// in the encryption preferences, unless the plain text format has been
// selected in the composer, in which case we must enforce PGP/INLINE.
if !b.isInternal() && b.shouldSign() && composerMIMEType == "text/plain" {
b.withScheme(pgpInline)
}
// If the sign flag (that we just determined above) is true, then the MIME // If the sign flag (that we just determined above) is true, then the MIME
// type is determined by the PGP scheme (also determined above): we should // type is determined by the PGP scheme (also determined above): we should
// use 'text/plain' for a PGP/Inline scheme, and 'multipart/mixed' otherwise. // use 'text/plain' for a PGP/Inline scheme, and 'multipart/mixed' otherwise.

View File

@ -51,7 +51,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: true, isInternal: true,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -66,7 +66,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{MIMEType: "text/plain"}, contactMeta: &ContactMetadata{MIMEType: "text/plain"},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: true, isInternal: true,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -81,7 +81,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testContactKey}}, contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: true, isInternal: true,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -97,7 +97,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}}, contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: true, isInternal: true,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -112,7 +112,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -127,7 +127,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{MIMEType: "text/plain"}, contactMeta: &ContactMetadata{MIMEType: "text/plain"},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -142,7 +142,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -157,7 +157,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Scheme: pgpInline}, contactMeta: &ContactMetadata{Scheme: pgpInline},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -172,7 +172,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Scheme: pgpMIME}, contactMeta: &ContactMetadata{Scheme: pgpMIME},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -187,7 +187,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testContactKey}}, contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -203,7 +203,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}}, contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}}, receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -218,7 +218,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{}, contactMeta: &ContactMetadata{},
receivedKeys: []pmapi.PublicKey{}, receivedKeys: []pmapi.PublicKey{},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: false, wantEncrypt: false,
wantSign: false, wantSign: false,
@ -232,7 +232,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{MIMEType: "text/plain"}, contactMeta: &ContactMetadata{MIMEType: "text/plain"},
receivedKeys: []pmapi.PublicKey{}, receivedKeys: []pmapi.PublicKey{},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: false, wantEncrypt: false,
wantSign: false, wantSign: false,
@ -243,10 +243,24 @@ func TestPreferencesBuilder(t *testing.T) {
{ {
name: "external with sign enabled", name: "external with sign enabled",
contactMeta: &ContactMetadata{Sign: true}, contactMeta: &ContactMetadata{Sign: true, SignIsSet: true},
receivedKeys: []pmapi.PublicKey{}, receivedKeys: []pmapi.PublicKey{},
isInternal: false, 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 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, wantEncrypt: false,
wantSign: true, wantSign: true,
@ -260,7 +274,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testContactKey}}, contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
receivedKeys: []pmapi.PublicKey{}, receivedKeys: []pmapi.PublicKey{},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: false, wantEncrypt: false,
wantSign: false, wantSign: false,
@ -272,10 +286,10 @@ func TestPreferencesBuilder(t *testing.T) {
{ {
name: "external with pinned contact public key, encrypted and signed", 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{}, receivedKeys: []pmapi.PublicKey{},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -287,10 +301,10 @@ func TestPreferencesBuilder(t *testing.T) {
{ {
name: "external with pinned contact public key, encrypted and signed using contact-specific pgp-inline", 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{}, receivedKeys: []pmapi.PublicKey{},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,
@ -302,10 +316,10 @@ func TestPreferencesBuilder(t *testing.T) {
{ {
name: "external with pinned contact public key, encrypted and signed using global pgp-inline", 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{}, receivedKeys: []pmapi.PublicKey{},
isInternal: false, isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal}, mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
wantEncrypt: true, wantEncrypt: true,
wantSign: true, wantSign: true,

View File

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

View File

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

View File

@ -21,10 +21,10 @@ package smtp
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"io" "io"
"mime" "mime"
"net/mail" "net/mail"
"regexp"
"strings" "strings"
"time" "time"
@ -179,11 +179,12 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
return 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) message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName)
if err != nil { if err != nil {
log.WithError(err).Error("Failed to parse message")
return return
} }
clearBody := message.Body clearBody := message.Body
@ -290,6 +291,9 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
} }
sendPreferences, err := su.getSendPreferences(email, message.MIMEType, mailSettings) sendPreferences, err := su.getSendPreferences(email, message.MIMEType, mailSettings)
if !sendPreferences.Encrypt {
containsUnencryptedRecipients = true
}
if err != nil { if err != nil {
return err return err
} }
@ -359,7 +363,9 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
return errors.New("error decoding subject message " + message.Header.Get("Subject")) return errors.New("error decoding subject message " + message.Header.Get("Subject"))
} }
if !su.continueSendingUnencryptedMail(subject) { if !su.continueSendingUnencryptedMail(subject) {
_ = su.client().DeleteMessages([]string{message.ID}) 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") return errors.New("sending was canceled by user")
} }
} }
@ -401,9 +407,9 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) { if !strings.Contains(reference, "@"+pmapi.InternalIDDomain) {
newReferences = append(newReferences, reference) newReferences = append(newReferences, reference)
} else { // internalid is the parentID. } else { // internalid is the parentID.
idMatch := regexp.MustCompile(pmapi.InternalReferenceFormat).FindStringSubmatch(reference) idMatch := pmapi.RxInternalReferenceFormat.FindStringSubmatch(reference)
if len(idMatch) > 0 { if len(idMatch) == 2 {
lastID := strings.TrimSuffix(strings.Trim(idMatch[0], "<>"), "@protonmail.internalid") lastID := idMatch[1]
filter := &pmapi.MessagesFilter{ID: []string{lastID}} filter := &pmapi.MessagesFilter{ID: []string{lastID}}
if su.addressID != "" { if su.addressID != "" {
filter.AddressID = su.addressID filter.AddressID = su.addressID

View File

@ -27,13 +27,13 @@ import (
) )
type ContactMetadata struct { type ContactMetadata struct {
Email string Email string
Keys []string Keys []string
Scheme string Scheme string
Sign bool Sign bool
SignMissing bool SignIsSet bool
Encrypt bool Encrypt bool
MIMEType string MIMEType string
} }
const ( 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. // 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 // However PMEL declares 'true' is true, 'false' is false. every other string is true
encrypt, _ := strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMEncrypt, group)) encrypt, _ := strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMEncrypt, group))
var sign, signMissing bool var sign, signIsSet bool
if len(parsedCard[FieldPMSign]) == 0 { if len(parsedCard[FieldPMSign]) == 0 {
signMissing = true signIsSet = false
} else { } else {
sign, _ = strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMSign, group)) sign, _ = strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMSign, group))
signMissing = false signIsSet = true
} }
mimeType := parsedCard.GetValueByGroup(FieldPMMIMEType, group) mimeType := parsedCard.GetValueByGroup(FieldPMMIMEType, group)
return &ContactMetadata{ return &ContactMetadata{
Email: email, Email: email,
Keys: keys, Keys: keys,
Scheme: scheme, Scheme: scheme,
Sign: sign, Sign: sign,
SignMissing: signMissing, SignIsSet: signIsSet,
Encrypt: encrypt, Encrypt: encrypt,
MIMEType: mimeType, MIMEType: mimeType,
}, nil }, nil
} }
return &ContactMetadata{}, nil return &ContactMetadata{}, nil

View File

@ -69,7 +69,7 @@ func (storeAddress *Address) init(foldersAndLabels []*pmapi.Label) (err error) {
prefix := getLabelPrefix(label) prefix := getLabelPrefix(label)
var mailbox *Mailbox 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. storeAddress.log.
WithError(err). WithError(err).
WithField("labelID", label.ID). WithField("labelID", label.ID).

View File

@ -73,14 +73,14 @@ func (storeAddress *Address) createOrUpdateMailboxEvent(label *pmapi.Label) erro
prefix := getLabelPrefix(label) prefix := getLabelPrefix(label)
mailbox, ok := storeAddress.mailboxes[label.ID] mailbox, ok := storeAddress.mailboxes[label.ID]
if !ok { 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 { if err != nil {
return err return err
} }
storeAddress.mailboxes[label.ID] = mailbox storeAddress.mailboxes[label.ID] = mailbox
mailbox.store.imapMailboxCreated(storeAddress.address, mailbox.labelName) mailbox.store.imapMailboxCreated(storeAddress.address, mailbox.labelName)
} else { } else {
mailbox.labelName = prefix + label.Name mailbox.labelName = prefix + label.Path
mailbox.color = label.Color mailbox.color = label.Color
} }
return nil return nil

View File

@ -45,7 +45,9 @@ func (c *Cache) getEventID(userID string) string {
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() 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 { if c.cache == nil {
c.cache = map[string]map[string]string{} c.cache = map[string]map[string]string{}

View File

@ -37,7 +37,7 @@ func (store *Store) SetIMAPUpdateChannel(updates chan imapBackend.Update) {
} }
} }
func (store *Store) imapNotice(address, notice string) { func (store *Store) imapNotice(address, notice string) *imapBackend.StatusUpdate {
update := new(imapBackend.StatusUpdate) update := new(imapBackend.StatusUpdate)
update.Update = imapBackend.NewUpdate(address, "") update.Update = imapBackend.NewUpdate(address, "")
update.StatusResp = &imap.StatusResp{ update.StatusResp = &imap.StatusResp{
@ -46,25 +46,35 @@ func (store *Store) imapNotice(address, notice string) {
Info: notice, Info: notice,
} }
store.imapSendUpdate(update) store.imapSendUpdate(update)
return update
} }
func (store *Store) imapUpdateMessage(address, mailboxName string, uid, sequenceNumber uint32, msg *pmapi.Message) { func (store *Store) imapUpdateMessage(
address, mailboxName string,
uid, sequenceNumber uint32,
msg *pmapi.Message, hasDeletedFlag bool,
) *imapBackend.MessageUpdate {
store.log.WithFields(logrus.Fields{ store.log.WithFields(logrus.Fields{
"address": address, "address": address,
"mailbox": mailboxName, "mailbox": mailboxName,
"seqNum": sequenceNumber, "seqNum": sequenceNumber,
"uid": uid, "uid": uid,
"flags": message.GetFlags(msg), "flags": message.GetFlags(msg),
"deleted": hasDeletedFlag,
}).Trace("IDLE update") }).Trace("IDLE update")
update := new(imapBackend.MessageUpdate) update := new(imapBackend.MessageUpdate)
update.Update = imapBackend.NewUpdate(address, mailboxName) update.Update = imapBackend.NewUpdate(address, mailboxName)
update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid}) update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
update.Message.Flags = message.GetFlags(msg) update.Message.Flags = message.GetFlags(msg)
if hasDeletedFlag {
update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag)
}
update.Message.Uid = uid update.Message.Uid = uid
store.imapSendUpdate(update) store.imapSendUpdate(update)
return update
} }
func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumber uint32) { func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumber uint32) *imapBackend.ExpungeUpdate {
store.log.WithFields(logrus.Fields{ store.log.WithFields(logrus.Fields{
"address": address, "address": address,
"mailbox": mailboxName, "mailbox": mailboxName,
@ -74,9 +84,10 @@ func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumbe
update.Update = imapBackend.NewUpdate(address, mailboxName) update.Update = imapBackend.NewUpdate(address, mailboxName)
update.SeqNum = sequenceNumber update.SeqNum = sequenceNumber
store.imapSendUpdate(update) store.imapSendUpdate(update)
return update
} }
func (store *Store) imapMailboxCreated(address, mailboxName string) { func (store *Store) imapMailboxCreated(address, mailboxName string) *imapBackend.MailboxInfoUpdate {
store.log.WithFields(logrus.Fields{ store.log.WithFields(logrus.Fields{
"address": address, "address": address,
"mailbox": mailboxName, "mailbox": mailboxName,
@ -89,9 +100,10 @@ func (store *Store) imapMailboxCreated(address, mailboxName string) {
Name: mailboxName, Name: mailboxName,
} }
store.imapSendUpdate(update) store.imapSendUpdate(update)
return update
} }
func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) { func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) *imapBackend.MailboxUpdate {
store.log.WithFields(logrus.Fields{ store.log.WithFields(logrus.Fields{
"address": address, "address": address,
"mailbox": mailboxName, "mailbox": mailboxName,
@ -106,6 +118,7 @@ func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread
update.MailboxStatus.Unseen = uint32(unread) update.MailboxStatus.Unseen = uint32(unread)
update.MailboxStatus.UnseenSeqNum = uint32(unreadSeqNum) update.MailboxStatus.UnseenSeqNum = uint32(unreadSeqNum)
store.imapSendUpdate(update) store.imapSendUpdate(update)
return update
} }
func (store *Store) imapSendUpdate(update imapBackend.Update) { func (store *Store) imapSendUpdate(update imapBackend.Update) {
@ -116,7 +129,7 @@ func (store *Store) imapSendUpdate(update imapBackend.Update) {
select { select {
case <-time.After(1 * time.Second): case <-time.After(1 * time.Second):
store.log.Error("Could not send IMAP update (timeout)") store.log.Warn("IMAP update could not be sent (timeout)")
return return
case store.imapUpdates <- update: case store.imapUpdates <- update:
} }

View File

@ -38,7 +38,8 @@ type eventLoop struct {
pollCh chan chan struct{} pollCh chan chan struct{}
stopCh chan struct{} stopCh chan struct{}
notifyStopCh 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 hasInternet bool
pollCounter int pollCounter int
@ -59,6 +60,7 @@ func newEventLoop(cache *Cache, store *Store, user BridgeUser, events listener.L
currentEventID: cache.getEventID(user.ID()), currentEventID: cache.getEventID(user.ID()),
pollCh: make(chan chan struct{}), pollCh: make(chan chan struct{}),
isRunning: false, isRunning: false,
isTickerPaused: false,
log: eventLog, 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 { func (loop *eventLoop) client() pmapi.Client {
return loop.store.client() return loop.store.client()
} }
@ -156,6 +154,10 @@ func (loop *eventLoop) loop() {
close(loop.notifyStopCh) close(loop.notifyStopCh)
return return
case <-t.C: 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. // 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) time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond)
case eventProcessedCh = <-loop.pollCh: case eventProcessedCh = <-loop.pollCh:

View File

@ -24,6 +24,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -102,10 +103,13 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) {
// Event loop runs in goroutine and will be stopped by deferred mock clearing. // Event loop runs in goroutine and will be stopped by deferred mock clearing.
go m.store.eventLoop.start() go m.store.eventLoop.start()
require.Eventually(t, func() bool { var err error
msg, err := m.store.getMessageFromDB("msg1") assert.Eventually(t, func() bool {
var msg *pmapi.Message
msg, err = m.store.getMessageFromDB("msg1")
return err == nil && msg.Subject == newSubject return err == nil && msg.Subject == newSubject
}, time.Second, 10*time.Millisecond) }, time.Second, 10*time.Millisecond)
require.NoError(t, err)
} }
func TestEventLoopUpdateMessage(t *testing.T) { func TestEventLoopUpdateMessage(t *testing.T) {

View File

@ -41,7 +41,7 @@ type Mailbox struct {
} }
func newMailbox(storeAddress *Address, labelID, labelPrefix, labelName, color string) (mb *Mailbox, err error) { func newMailbox(storeAddress *Address, labelID, labelPrefix, labelName, color string) (mb *Mailbox, err error) {
_ = 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) mb, err = txNewMailbox(tx, storeAddress, labelID, labelPrefix, labelName, color)
return err return err
}) })
@ -142,6 +142,9 @@ func initMailboxBucket(tx *bolt.Tx, bucketName []byte) error {
if _, err := bucket.CreateBucketIfNotExists(apiIDsBucket); err != nil { if _, err := bucket.CreateBucketIfNotExists(apiIDsBucket); err != nil {
return err return err
} }
if _, err := bucket.CreateBucketIfNotExists(deletedIDsBucket); err != nil {
return err
}
return nil return nil
} }
@ -238,6 +241,11 @@ func (storeMailbox *Mailbox) txGetAPIIDsBucket(tx *bolt.Tx) *bolt.Bucket {
return storeMailbox.txGetBucket(tx).Bucket(apiIDsBucket) 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. // txGetBucket returns the bucket of mailbox containing mapping buckets.
func (storeMailbox *Mailbox) txGetBucket(tx *bolt.Tx) *bolt.Bucket { func (storeMailbox *Mailbox) txGetBucket(tx *bolt.Tx) *bolt.Bucket {
return tx.Bucket(mailboxesBucket).Bucket(storeMailbox.getBucketName()) return tx.Bucket(mailboxesBucket).Bucket(storeMailbox.getBucketName())

View File

@ -125,6 +125,7 @@ func (mc *mailboxCounts) getPMLabel() *pmapi.Label {
return &pmapi.Label{ return &pmapi.Label{
ID: mc.LabelID, ID: mc.LabelID,
Name: mc.LabelName, Name: mc.LabelName,
Path: mc.LabelName,
Color: mc.Color, Color: mc.Color,
Order: mc.Order, Order: mc.Order,
Type: pmapi.LabelTypeMailbox, Type: pmapi.LabelTypeMailbox,
@ -158,7 +159,7 @@ func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) er
} }
// Update mailbox info, but dont change on-API-counts. // Update mailbox info, but dont change on-API-counts.
mailbox.LabelName = label.Name mailbox.LabelName = label.Path
mailbox.Color = label.Color mailbox.Color = label.Color
mailbox.Order = label.Order mailbox.Order = label.Order
mailbox.IsFolder = label.Exclusive == 1 mailbox.IsFolder = label.Exclusive == 1

View File

@ -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) { 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)) v := b.Get([]byte(apiID))
if v == nil { if v == nil {
return 0, ErrNoSuchAPIID return 0, ErrNoSuchAPIID
@ -137,6 +141,19 @@ func (storeMailbox *Mailbox) txGetUID(tx *bolt.Tx, apiID string) (uint32, error)
return btoi(v), nil 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`. // 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) { func (storeMailbox *Mailbox) getSequenceNumber(apiID string) (seqNum uint32, err error) {
err = storeMailbox.db().View(func(tx *bolt.Tx) error { err = storeMailbox.db().View(func(tx *bolt.Tx) error {

View File

@ -18,12 +18,16 @@
package store package store
import ( import (
"time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt" 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") var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder")
// GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage` // GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage`
@ -96,11 +100,8 @@ func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error {
// It has to be propagated to all the same messages in all mailboxes. // It has to be propagated to all the same messages in all mailboxes.
// The propagation is processed by the event loop. // The propagation is processed by the event loop.
func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error { func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error {
log.WithFields(logrus.Fields{ storeMailbox.log.WithField("messages", apiIDs).
"messages": apiIDs, Trace("Unlabeling messages")
"label": storeMailbox.labelID,
"mailbox": storeMailbox.Name,
}).Trace("Unlabeling messages")
if storeMailbox.labelID == pmapi.AllMailLabel { if storeMailbox.labelID == pmapi.AllMailLabel {
return ErrAllMailOpNotAllowed return ErrAllMailOpNotAllowed
} }
@ -129,6 +130,9 @@ func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error {
ids = append(ids, apiID) ids = append(ids, apiID)
} }
} }
if len(ids) == 0 {
return nil
}
return storeMailbox.client().MarkMessagesRead(ids) return storeMailbox.client().MarkMessagesRead(ids)
} }
@ -170,54 +174,63 @@ func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error {
return storeMailbox.client().UnlabelMessages(apiIDs, pmapi.StarredLabel) return storeMailbox.client().UnlabelMessages(apiIDs, pmapi.StarredLabel)
} }
// DeleteMessages deletes messages. // MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API
// If the mailbox is All Mail or All Sent, it does nothing. // until RemoveDeleted is called
// If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted. func (storeMailbox *Mailbox) MarkMessagesDeleted(apiIDs []string) error {
// In all other cases the message is only removed from the mailbox.
func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"messages": apiIDs, "messages": apiIDs,
"label": storeMailbox.labelID, "label": storeMailbox.labelID,
"mailbox": storeMailbox.Name, "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() defer storeMailbox.pollNow()
switch storeMailbox.labelID { switch storeMailbox.labelID {
case pmapi.AllMailLabel, pmapi.AllSentLabel: case pmapi.AllMailLabel, pmapi.AllSentLabel:
break break
case pmapi.TrashLabel, pmapi.SpamLabel: case pmapi.TrashLabel, pmapi.SpamLabel:
messageIDsToDelete := []string{} if err := storeMailbox.deleteFromTrashOrSpam(apiIDs); err != nil {
messageIDsToUnlabel := []string{} return err
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
}
} }
case pmapi.DraftLabel: case pmapi.DraftLabel:
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil { if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
@ -231,6 +244,50 @@ func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
return nil 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 {
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
return err
}
}
return nil
}
func (storeMailbox *Mailbox) txSkipAndRemoveFromMailbox(tx *bolt.Tx, msg *pmapi.Message) (skipAndRemove bool) { func (storeMailbox *Mailbox) txSkipAndRemoveFromMailbox(tx *bolt.Tx, msg *pmapi.Message) (skipAndRemove bool) {
defer func() { defer func() {
if skipAndRemove { 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. // Buckets are not initialized right away because it's a heavy operation.
// The best option is to get the same bucket only once and only when needed. // The best option is to get the same bucket only once and only when needed.
var apiBucket, imapBucket *bolt.Bucket var apiBucket, imapBucket, deletedBucket *bolt.Bucket
for _, msg := range msgs { for _, msg := range msgs {
if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) { if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) {
continue continue
@ -289,12 +346,15 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
} }
} else { } else {
uidb := apiBucket.Get([]byte(msg.ID)) uidb := apiBucket.Get([]byte(msg.ID))
if uidb != nil { if uidb != nil {
if imapBucket == nil { if imapBucket == nil {
imapBucket = storeMailbox.txGetIMAPIDsBucket(tx) imapBucket = storeMailbox.txGetIMAPIDsBucket(tx)
} }
seqNum, seqErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb) seqNum, seqErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb)
if deletedBucket == nil {
deletedBucket = storeMailbox.txGetDeletedIDsBucket(tx)
}
isMarkedAsDeleted := deletedBucket.Get([]byte(msg.ID)) != nil
if seqErr == nil { if seqErr == nil {
storeMailbox.store.imapUpdateMessage( storeMailbox.store.imapUpdateMessage(
storeMailbox.storeAddress.address, storeMailbox.storeAddress.address,
@ -302,6 +362,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
btoi(uidb), btoi(uidb),
seqNum, seqNum,
msg, msg,
isMarkedAsDeleted,
) )
} }
continue continue
@ -335,6 +396,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
uid, uid,
seqNum, seqNum,
msg, msg,
false, // new message is never marked as deleted
) )
shouldSendMailboxUpdate = true shouldSendMailboxUpdate = true
} }
@ -359,6 +421,7 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
} }
imapBucket := storeMailbox.txGetIMAPIDsBucket(tx) imapBucket := storeMailbox.txGetIMAPIDsBucket(tx)
deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx)
seqNum, seqNumErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb) seqNum, seqNumErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb)
if seqNumErr != nil { if seqNumErr != nil {
@ -373,6 +436,10 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
return errors.Wrap(err, "cannot delete from API bucket") 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 { if seqNumErr == nil {
storeMailbox.store.imapDeleteMessage( storeMailbox.store.imapDeleteMessage(
storeMailbox.storeAddress.address, storeMailbox.storeAddress.address,
@ -401,3 +468,58 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
) )
return nil 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))
update := storeMailbox.store.imapUpdateMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
uid,
seqNum,
msg,
markAsDeleted,
)
// txMarkMessagesAsDeleted is called only during processing request
// from IMAP call (i.e., not from event loop) and in such cases we
// have to wait to propagate update back before closing the response.
select {
case <-time.After(1 * time.Second):
case <-update.Done():
}
}
return nil
}

View File

@ -62,6 +62,21 @@ func (message *Message) Message() *pmapi.Message {
return message.msg 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 // SetSize updates the information about size of decrypted message which can be
// used for IMAP. This should not trigger any IMAP update. // used for IMAP. This should not trigger any IMAP update.
// NOTE: The size from the server corresponds to pure body bytes. Hence it // NOTE: The size from the server corresponds to pure body bytes. Hence it

View File

@ -70,6 +70,8 @@ var (
// * {imapUID} -> string messageID // * {imapUID} -> string messageID
// * api_ids // * api_ids
// * {messageID} -> uint32 imapUID // * {messageID} -> uint32 imapUID
// * deleted_ids (can be missing or have no keys)
// * {messageID} -> true
metadataBucket = []byte("metadata") //nolint[gochecknoglobals] metadataBucket = []byte("metadata") //nolint[gochecknoglobals]
countsBucket = []byte("counts") //nolint[gochecknoglobals] countsBucket = []byte("counts") //nolint[gochecknoglobals]
addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals] addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals]
@ -78,6 +80,7 @@ var (
mailboxesBucket = []byte("mailboxes") //nolint[gochecknoglobals] mailboxesBucket = []byte("mailboxes") //nolint[gochecknoglobals]
imapIDsBucket = []byte("imap_ids") //nolint[gochecknoglobals] imapIDsBucket = []byte("imap_ids") //nolint[gochecknoglobals]
apiIDsBucket = []byte("api_ids") //nolint[gochecknoglobals] apiIDsBucket = []byte("api_ids") //nolint[gochecknoglobals]
deletedIDsBucket = []byte("deleted_ids") //nolint[gochecknoglobals]
mboxVersionBucket = []byte("mailboxes_version") //nolint[gochecknoglobals] mboxVersionBucket = []byte("mailboxes_version") //nolint[gochecknoglobals]
// ErrNoSuchAPIID when mailbox does not have API ID. // ErrNoSuchAPIID when mailbox does not have API ID.
@ -348,6 +351,18 @@ func (store *Store) addAddress(address, addressID string, labels []*pmapi.Label)
return 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. // Close stops the event loop and closes the database to free the file.
func (store *Store) Close() error { func (store *Store) Close() error {
store.lock.Lock() store.lock.Lock()

View File

@ -26,6 +26,10 @@ import (
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
func (loop *eventLoop) IsRunning() bool {
return loop.isRunning
}
// TestSync triggers a sync of the store. // TestSync triggers a sync of the store.
func (store *Store) TestSync() { func (store *Store) TestSync() {
store.lock.Lock() 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 { err := mailboxes.ForEach(func(mboxName, mboxData []byte) error {
fmt.Println("mbox:", string(mboxName)) fmt.Println("mbox:", string(mboxName))
b := mailboxes.Bucket(mboxName).Bucket(imapIDsBucket) b := mailboxes.Bucket(mboxName).Bucket(imapIDsBucket)
deletedMailboxes := mailboxes.Bucket(mboxName).Bucket(deletedIDsBucket)
c := b.Cursor() c := b.Cursor()
i := 0 i := 0
for imapID, apiID := c.First(); imapID != nil; imapID, apiID = c.Next() { for imapID, apiID := c.First(); imapID != nil; imapID, apiID = c.Next() {
i++ 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) data := metadata.Get(apiID)
if !assert.NotNil(tb, data) { if !assert.NotNil(tb, data) {
continue continue

View File

@ -210,7 +210,9 @@ func (store *Store) deleteMailboxEvent(labelID string) error {
store.lock.Lock() store.lock.Lock()
defer store.lock.Unlock() 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 { for _, a := range store.addresses {
if err := a.deleteMailboxEvent(labelID); err != nil { if err := a.deleteMailboxEvent(labelID); err != nil {

View File

@ -63,7 +63,7 @@ func (store *Store) CreateDraft(
attachmentReaders = append(attachmentReaders, strings.NewReader(attachedPublicKey)) attachmentReaders = append(attachmentReaders, strings.NewReader(attachedPublicKey))
publicKeyAttachment := &pmapi.Attachment{ publicKeyAttachment := &pmapi.Attachment{
Name: attachedPublicKeyName + ".asc", Name: attachedPublicKeyName + ".asc",
MIMEType: "application/pgp-key", MIMEType: "application/pgp-keys",
Header: textproto.MIMEHeader{}, Header: textproto.MIMEHeader{},
} }
attachments = append(attachments, publicKeyAttachment) 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) { 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)) msgb := b.Get([]byte(apiID))
if msgb == nil { if msgb == nil {
return nil, ErrNoSuchAPIID return nil, ErrNoSuchAPIID

View File

@ -29,17 +29,34 @@ type Message struct {
ID string ID string
Unread bool Unread bool
Body []byte Body []byte
Source Mailbox Sources []Mailbox
Targets []Mailbox Targets []Mailbox
} }
// sourceNames returns array of source mailbox names.
func (msg Message) sourceNames() (names []string) {
for _, mailbox := range msg.Sources {
names = append(names, mailbox.Name)
}
return
}
// targetNames returns array of target mailbox names.
func (msg Message) targetNames() (names []string) {
for _, mailbox := range msg.Targets {
names = append(names, mailbox.Name)
}
return
}
// MessageStatus holds status for message used by progress manager. // MessageStatus holds status for message used by progress manager.
type MessageStatus struct { type MessageStatus struct {
eventTime time.Time // Time of adding message to the process. eventTime time.Time // Time of adding message to the process.
rule *Rule // Rule with source and target mailboxes. sourceNames []string // Source mailbox names message is in.
SourceID string // Message ID at the source. SourceID string // Message ID at the source.
targetID string // Message ID at the target (if any). targetNames []string // Target mailbox names message is in.
bodyHash string // Hash of the message body. targetID string // Message ID at the target (if any).
bodyHash string // Hash of the message body.
exported bool exported bool
imported bool imported bool

View File

@ -62,11 +62,20 @@ func (p *Progress) update() {
return return
} }
// In case no one listens for an update, do not block the progress. // In case no one listens for an update, do not block the whole progress.
select { go func() {
case p.updateCh <- struct{}{}: defer func() {
case <-time.After(100 * time.Millisecond): // 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. // 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() defer p.lock.Unlock()
log.WithError(err).Error("Progress finished") log.WithError(err).Error("Progress finished")
p.isStopped = true p.setStop()
p.fatalError = err p.fatalError = err
p.cleanUpdateCh() p.cleanUpdateCh()
} }
@ -117,16 +126,17 @@ func (p *Progress) updateCount(mailbox string, count uint) {
} }
// addMessage should be called as soon as there is ID of the message. // addMessage should be called as soon as there is ID of the message.
func (p *Progress) addMessage(messageID string, rule *Rule) { func (p *Progress) addMessage(messageID string, sourceNames, targetNames []string) {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
defer p.update() defer p.update()
p.log.WithField("id", messageID).Trace("Message added") p.log.WithField("id", messageID).Trace("Message added")
p.messageStatuses[messageID] = &MessageStatus{ p.messageStatuses[messageID] = &MessageStatus{
eventTime: time.Now(), eventTime: time.Now(),
rule: rule, sourceNames: sourceNames,
SourceID: messageID, SourceID: messageID,
targetNames: targetNames,
} }
} }
@ -273,6 +283,15 @@ func (p *Progress) Stop() {
defer p.update() defer p.update()
p.log.Info("Progress stopped") p.log.Info("Progress stopped")
p.setStop()
// Once progress is stopped, some calls might be in progress. Results from
// those calls are irrelevant so we can close update channel sooner to not
// propagate any progress to user interface anymore.
p.cleanUpdateCh()
}
func (p *Progress) setStop() {
p.isStopped = true p.isStopped = true
p.pauseReason = "" // Clear pause to run paused code and stop it. p.pauseReason = "" // Clear pause to run paused code and stop it.
} }

View File

@ -19,6 +19,7 @@ package transfer
import ( import (
"testing" "testing"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
a "github.com/stretchr/testify/assert" a "github.com/stretchr/testify/assert"
@ -47,21 +48,21 @@ func TestProgressAddingMessages(t *testing.T) {
drainProgressUpdateChannel(&progress) drainProgressUpdateChannel(&progress)
// msg1 has no problem. // msg1 has no problem.
progress.addMessage("msg1", nil) progress.addMessage("msg1", []string{}, []string{})
progress.messageExported("msg1", []byte(""), nil) progress.messageExported("msg1", []byte(""), nil)
progress.messageImported("msg1", "", nil) progress.messageImported("msg1", "", nil)
// msg2 has an import problem. // msg2 has an import problem.
progress.addMessage("msg2", nil) progress.addMessage("msg2", []string{}, []string{})
progress.messageExported("msg2", []byte(""), nil) progress.messageExported("msg2", []byte(""), nil)
progress.messageImported("msg2", "", errors.New("failed import")) progress.messageImported("msg2", "", errors.New("failed import"))
// msg3 has an export problem. // msg3 has an export problem.
progress.addMessage("msg3", nil) progress.addMessage("msg3", []string{}, []string{})
progress.messageExported("msg3", []byte(""), errors.New("failed export")) progress.messageExported("msg3", []byte(""), errors.New("failed export"))
// msg4 has an export problem and import is also called. // msg4 has an export problem and import is also called.
progress.addMessage("msg4", nil) progress.addMessage("msg4", []string{}, []string{})
progress.messageExported("msg4", []byte(""), errors.New("failed export")) progress.messageExported("msg4", []byte(""), errors.New("failed export"))
progress.messageImported("msg4", "", nil) progress.messageImported("msg4", "", nil)
@ -91,7 +92,7 @@ func TestProgressFinish(t *testing.T) {
progress.finish() progress.finish()
r.Nil(t, progress.updateCh) r.Nil(t, progress.updateCh)
r.NotPanics(t, func() { progress.addMessage("msg", nil) }) r.NotPanics(t, func() { progress.addMessage("msg", []string{}, []string{}) })
} }
func TestProgressFatalError(t *testing.T) { func TestProgressFatalError(t *testing.T) {
@ -101,7 +102,29 @@ func TestProgressFatalError(t *testing.T) {
progress.fatal(errors.New("fatal error")) progress.fatal(errors.New("fatal error"))
r.Nil(t, progress.updateCh) r.Nil(t, progress.updateCh)
r.NotPanics(t, func() { progress.addMessage("msg", nil) }) r.NotPanics(t, func() { progress.addMessage("msg", []string{}, []string{}) })
}
func TestFailUnpauseAndStops(t *testing.T) {
progress := newProgress(log, nil)
drainProgressUpdateChannel(&progress)
progress.Pause("pausing")
progress.fatal(errors.New("fatal error"))
r.Nil(t, progress.updateCh)
r.True(t, progress.isStopped)
r.False(t, progress.IsPaused())
r.Eventually(t, progress.shouldStop, time.Second, 10*time.Millisecond)
}
func TestStopClosesUpdates(t *testing.T) {
progress := newProgress(log, nil)
ch := progress.updateCh
progress.Stop()
r.Nil(t, progress.updateCh)
r.PanicsWithError(t, "send on closed channel", func() { ch <- struct{}{} })
} }
func drainProgressUpdateChannel(progress *Progress) { func drainProgressUpdateChannel(progress *Progress) {

View File

@ -109,7 +109,7 @@ func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *P
// addMessage is called after time check to not report message // addMessage is called after time check to not report message
// which should not be exported but any error from reading body // which should not be exported but any error from reading body
// or parsing time is reported as an error. // or parsing time is reported as an error.
progress.addMessage(filePath, rule) progress.addMessage(filePath, msg.sourceNames(), msg.targetNames())
progress.messageExported(filePath, msg.Body, err) progress.messageExported(filePath, msg.Body, err)
if err == nil { if err == nil {
ch <- msg ch <- msg
@ -134,7 +134,7 @@ func (p *EMLProvider) exportMessage(rule *Rule, filePath string) (Message, error
ID: filePath, ID: filePath,
Unread: false, Unread: false,
Body: body, Body: body,
Source: rule.SourceMailbox, Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes, Targets: rule.TargetMailboxes,
}, nil }, nil
} }

View File

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

View File

@ -31,6 +31,8 @@ type IMAPProvider struct {
addr string addr string
client *imapClient.Client client *imapClient.Client
timeIt *timeIt
} }
// NewIMAPProvider returns new IMAPProvider. // NewIMAPProvider returns new IMAPProvider.
@ -39,6 +41,8 @@ func NewIMAPProvider(username, password, host, port string) (*IMAPProvider, erro
username: username, username: username,
password: password, password: password,
addr: net.JoinHostPort(host, port), addr: net.JoinHostPort(host, port),
timeIt: newTimeIt("imap"),
} }
if err := p.auth(); err != nil { if err := p.auth(); err != nil {

View File

@ -40,6 +40,9 @@ func (p *IMAPProvider) TransferTo(rules transferRules, progress *Progress, ch ch
log.Info("Started transfer from IMAP to channel") log.Info("Started transfer from IMAP to channel")
defer log.Info("Finished 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) imapMessageInfoMap := p.loadMessageInfoMap(rules, progress)
for rule := range rules.iterateActiveRules() { for rule := range rules.iterateActiveRules() {
@ -78,6 +81,9 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres
} }
func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValidity, count uint32) map[string]imapMessageInfo { 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)
messagesInfo := map[string]imapMessageInfo{} messagesInfo := map[string]imapMessageInfo{}
pageStart := uint32(1) pageStart := uint32(1)
@ -118,7 +124,7 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
uid: imapMessage.Uid, uid: imapMessage.Uid,
size: imapMessage.Size, size: imapMessage.Size,
} }
progress.addMessage(id, rule) progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
} }
progress.callWrap(func() error { progress.callWrap(func() error {
@ -199,13 +205,18 @@ func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<-
progress.messageExported(id, body, err) progress.messageExported(id, body, err)
if err == nil { if err == nil {
msg := p.exportMessage(rule, id, imapMessage, body) msg := p.exportMessage(rule, id, imapMessage, body)
p.timeIt.stop("fetch", rule.SourceMailbox.Name)
ch <- msg ch <- msg
p.timeIt.start("fetch", rule.SourceMailbox.Name)
} }
} }
p.timeIt.start("fetch", rule.SourceMailbox.Name)
progress.callWrap(func() error { progress.callWrap(func() error {
return p.uidFetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback) 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 { func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Message, body []byte) Message {
@ -220,7 +231,7 @@ func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Me
ID: id, ID: id,
Unread: unread, Unread: unread,
Body: body, Body: body,
Source: rule.SourceMailbox, Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes, Targets: rule.TargetMailboxes,
} }
} }

View File

@ -18,7 +18,9 @@
package transfer package transfer
import ( import (
"crypto/tls"
"net" "net"
"strings"
"time" "time"
imapID "github.com/ProtonMail/go-imap-id" imapID "github.com/ProtonMail/go-imap-id"
@ -146,7 +148,19 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
if host == "127.0.0.1" { if host == "127.0.0.1" {
client, err = imapClient.Dial(p.addr) client, err = imapClient.Dial(p.addr)
} else { } else {
client, err = imapClient.DialTLS(p.addr, nil) // IMAP.mail.yahoo.com have problem with golang TLS1.3
// implementation with weird behaviour i.e. Yahoo
// no error during dial or handshake but server logs out right
// after successful login leaving no time to perform any
// action. It was discovered that limiting to maximum TLS
// version 1.2 for yahoo servers is working solution.
var tlsConf *tls.Config
if strings.Contains(strings.ToLower(host), "yahoo") {
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
}
client, err = imapClient.DialTLS(p.addr, tlsConf)
} }
if err != nil { if err != nil {
return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}} return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}}

View File

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

View File

@ -0,0 +1,118 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package transfer
import (
"bufio"
"bytes"
"io"
"mime"
"os"
"strings"
)
type stringSet map[string]bool
const xGmailLabelsHeader = "X-Gmail-Labels"
// filteredOutGmailLabels is set of labels which we don't want to show to users
// as they might be auto-generated by Gmail and unwanted.
var filteredOutGmailLabels = []string{ //nolint[gochecknoglobals]
"Unread",
"Opened",
"IMAP_Junk",
"IMAP_NonJunk",
"IMAP_NotJunk",
"IMAP_$NotJunk",
}
func getGmailLabelsFromMboxFile(filePath string) (stringSet, error) {
f, err := os.Open(filePath) //nolint[gosec]
if err != nil {
return nil, err
}
return getGmailLabelsFromMboxReader(f)
}
func getGmailLabelsFromMboxReader(f io.Reader) (stringSet, error) {
allLabels := stringSet{}
// Scanner is not used as it does not support long lines and some mbox
// files contain very long lines even though that should not be happening.
r := bufio.NewReader(f)
for {
b, isPrefix, err := r.ReadLine()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
for isPrefix {
_, isPrefix, err = r.ReadLine()
if err != nil {
break
}
}
if bytes.HasPrefix(b, []byte(xGmailLabelsHeader)) {
for label := range getGmailLabelsFromValue(string(b)) {
allLabels[label] = true
}
}
}
return allLabels, nil
}
func getGmailLabelsFromMessage(body []byte) (stringSet, error) {
header, err := getMessageHeader(body)
if err != nil {
return nil, err
}
labels := header.Get(xGmailLabelsHeader)
return getGmailLabelsFromValue(labels), nil
}
func getGmailLabelsFromValue(value string) stringSet {
value = strings.TrimPrefix(value, xGmailLabelsHeader+":")
if decoded, err := new(mime.WordDecoder).DecodeHeader(value); err != nil {
log.WithError(err).Error("Failed to decode header")
} else {
value = decoded
}
labels := stringSet{}
for _, label := range strings.Split(value, ",") {
label = strings.TrimSpace(label)
if label == "" {
continue
}
skip := false
for _, filteredOutLabel := range filteredOutGmailLabels {
if label == filteredOutLabel {
skip = true
break
}
}
if skip {
continue
}
labels[label] = true
}
return labels
}

View File

@ -0,0 +1,135 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package transfer
import (
"fmt"
"strings"
"testing"
r "github.com/stretchr/testify/require"
)
func TestGetGmailLabelsFromMboxReader(t *testing.T) {
mboxFile := `From - Mon May 4 16:40:31 2020
Subject: Test 1
X-Gmail-Labels: Foo,Bar
hello
From - Mon May 4 16:40:31 2020
Subject: Test 2
X-Gmail-Labels: Foo , Baz
hello
From - Mon May 4 16:40:31 2020
Subject: Test 3
X-Gmail-Labels: ,
hello
From - Mon May 4 16:40:31 2020
Subject: Test 4
X-Gmail-Labels:
hello
From - Mon May 4 16:40:31 2020
Subject: Test 5
hello
`
mboxReader := strings.NewReader(mboxFile)
labels, err := getGmailLabelsFromMboxReader(mboxReader)
r.NoError(t, err)
r.Equal(t, toSet("Foo", "Bar", "Baz"), labels)
}
func TestGetGmailLabelsFromMessage(t *testing.T) {
tests := []struct {
body string
wantLabels stringSet
}{
{`Subject: One
X-Gmail-Labels: Foo,Bar
Hello
`, toSet("Foo", "Bar")},
{`Subject: Two
X-Gmail-Labels: Foo , Bar ,
Hello
`, toSet("Foo", "Bar")},
{`Subject: Three
X-Gmail-Labels: ,
Hello
`, toSet()},
{`Subject: Four
X-Gmail-Labels:
Hello
`, toSet()},
{`Subject: Five
Hello
`, toSet()},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%v", tc.body), func(t *testing.T) {
labels, err := getGmailLabelsFromMessage([]byte(tc.body))
r.NoError(t, err)
r.Equal(t, tc.wantLabels, labels)
})
}
}
func TestGetGmailLabelsFromValue(t *testing.T) {
tests := []struct {
value string
wantLabels stringSet
}{
{"Foo,Bar", toSet("Foo", "Bar")},
{" Foo , Bar ", toSet("Foo", "Bar")},
{" Foo , Bar , ", toSet("Foo", "Bar")},
{" Foo Bar ", toSet("Foo Bar")},
{" , ", toSet()},
{" ", toSet()},
{"", toSet()},
{"=?UTF-8?Q?Archived,Category_personal,test_=F0=9F=98=80=F0=9F=99=83?=", toSet("Archived", "Category personal", "test 😀🙃")},
{"IMAP_NotJunk,Foo,Opened,bar,Unread", toSet("Foo", "bar")},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%v", tc.value), func(t *testing.T) {
labels := getGmailLabelsFromValue(tc.value)
r.Equal(t, tc.wantLabels, labels)
})
}
}
func toSet(items ...string) stringSet {
set := map[string]bool{}
for _, item := range items {
set[item] = true
}
return set
}

View File

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

View File

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

View File

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

View File

@ -33,8 +33,10 @@ type PMAPIProvider struct {
addressID string addressID string
keyRing *crypto.KeyRing keyRing *crypto.KeyRing
importMsgReqMap map[string]*pmapi.ImportMsgReq // Key is msg transfer ID. nextImportRequests map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
importMsgReqSize int nextImportRequestsSize int
timeIt *timeIt
} }
// NewPMAPIProvider returns new PMAPIProvider. // NewPMAPIProvider returns new PMAPIProvider.
@ -45,8 +47,10 @@ func NewPMAPIProvider(config *pmapi.ClientConfig, clientManager ClientManager, u
userID: userID, userID: userID,
addressID: addressID, addressID: addressID,
importMsgReqMap: map[string]*pmapi.ImportMsgReq{}, nextImportRequests: map[string]*pmapi.ImportMsgReq{},
importMsgReqSize: 0, nextImportRequestsSize: 0,
timeIt: newTimeIt("pmapi"),
} }
if addressID != "" { if addressID != "" {

View File

@ -34,6 +34,9 @@ func (p *PMAPIProvider) TransferTo(rules transferRules, progress *Progress, ch c
log.Info("Started transfer from PMAPI to channel") log.Info("Started transfer from PMAPI to channel")
defer log.Info("Finished transfer from PMAPI to channel") defer log.Info("Finished transfer from PMAPI to channel")
p.timeIt.clear()
defer p.timeIt.logResults()
// TransferTo cannot end sooner than loadCounts goroutine because // TransferTo cannot end sooner than loadCounts goroutine because
// loadCounts writes to channel in progress which would be closed. // loadCounts writes to channel in progress which would be closed.
// That can happen for really small accounts. // That can happen for really small accounts.
@ -120,7 +123,7 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
} }
msgID := fmt.Sprintf("%s_%s", rule.SourceMailbox.ID, pmapiMessage.ID) msgID := fmt.Sprintf("%s_%s", rule.SourceMailbox.ID, pmapiMessage.ID)
progress.addMessage(msgID, rule) progress.addMessage(msgID, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
msg, err := p.exportMessage(rule, progress, pmapiMessage.ID, msgID, skipEncryptedMessages) msg, err := p.exportMessage(rule, progress, pmapiMessage.ID, msgID, skipEncryptedMessages)
progress.messageExported(msgID, msg.Body, err) progress.messageExported(msgID, msg.Body, err)
if err == nil { if err == nil {
@ -147,6 +150,9 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
return err return err
}) })
p.timeIt.start("build", msgID)
defer p.timeIt.stop("build", msgID)
msgBuilder := pkgMessage.NewBuilder(p.client(), msg) msgBuilder := pkgMessage.NewBuilder(p.client(), msg)
msgBuilder.EncryptedToHTML = false msgBuilder.EncryptedToHTML = false
_, body, err := msgBuilder.BuildMessage() _, body, err := msgBuilder.BuildMessage()
@ -171,7 +177,7 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
ID: msgID, ID: msgID,
Unread: unread, Unread: unread,
Body: body, Body: body,
Source: rule.SourceMailbox, Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes, Targets: rule.TargetMailboxes,
}, nil }, nil
} }

View File

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"sync"
pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message" pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -32,6 +33,7 @@ import (
const ( const (
pmapiImportBatchMaxItems = 10 pmapiImportBatchMaxItems = 10
pmapiImportBatchMaxSize = 25 * 1000 * 1000 // 25 MB pmapiImportBatchMaxSize = 25 * 1000 * 1000 // 25 MB
pmapiImportWorkers = 4 // To keep memory under 1 GB.
) )
// DefaultMailboxes returns the default mailboxes for default rules if no other is found. // DefaultMailboxes returns the default mailboxes for default rules if no other is found.
@ -72,10 +74,16 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
log.Info("Started transfer from channel to PMAPI") log.Info("Started transfer from channel to PMAPI")
defer log.Info("Finished transfer from channel to PMAPI") defer log.Info("Finished transfer from channel to PMAPI")
p.timeIt.clear()
defer p.timeIt.logResults()
// Cache has to be cleared before each transfer to not contain // Cache has to be cleared before each transfer to not contain
// old stuff from previous cancelled run. // old stuff from previous cancelled run.
p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{} p.nextImportRequests = map[string]*pmapi.ImportMsgReq{}
p.importMsgReqSize = 0 p.nextImportRequestsSize = 0
preparedImportRequestsCh := make(chan map[string]*pmapi.ImportMsgReq)
wg := p.startImportWorkers(progress, preparedImportRequestsCh)
for msg := range ch { for msg := range ch {
if progress.shouldStop() { if progress.shouldStop() {
@ -85,13 +93,15 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
if p.isMessageDraft(msg) { if p.isMessageDraft(msg) {
p.transferDraft(rules, progress, msg) p.transferDraft(rules, progress, msg)
} else { } else {
p.transferMessage(rules, progress, msg) p.transferMessage(rules, progress, msg, preparedImportRequestsCh)
} }
} }
if len(p.importMsgReqMap) > 0 { if len(p.nextImportRequests) > 0 {
p.importMessages(progress) preparedImportRequestsCh <- p.nextImportRequests
} }
close(preparedImportRequestsCh)
wg.Wait()
} }
func (p *PMAPIProvider) isMessageDraft(msg Message) bool { func (p *PMAPIProvider) isMessageDraft(msg Message) bool {
@ -114,7 +124,10 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
return "", errors.Wrap(err, "failed to parse message") return "", errors.Wrap(err, "failed to parse message")
} }
if err := message.Encrypt(p.keyRing, nil); err != nil { p.timeIt.start("encrypt", msg.ID)
err = message.Encrypt(p.keyRing, nil)
p.timeIt.stop("encrypt", msg.ID)
if err != nil {
return "", errors.Wrap(err, "failed to encrypt draft") return "", errors.Wrap(err, "failed to encrypt draft")
} }
@ -125,7 +138,7 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
attachments := message.Attachments attachments := message.Attachments
message.Attachments = nil message.Attachments = nil
draft, err := p.createDraft(message, "", pmapi.DraftActionReply) draft, err := p.createDraft(msg.ID, message, "", pmapi.DraftActionReply)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to create draft") return "", errors.Wrap(err, "failed to create draft")
} }
@ -140,13 +153,15 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
return "", errors.Wrap(err, "failed to sign attachment") return "", errors.Wrap(err, "failed to sign attachment")
} }
p.timeIt.start("encrypt", msg.ID)
r = bytes.NewReader(attachmentBody) r = bytes.NewReader(attachmentBody)
encReader, err := attachment.Encrypt(p.keyRing, r) encReader, err := attachment.Encrypt(p.keyRing, r)
p.timeIt.stop("encrypt", msg.ID)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to encrypt attachment") return "", errors.Wrap(err, "failed to encrypt attachment")
} }
_, err = p.createAttachment(attachment, encReader, sigReader) _, err = p.createAttachment(msg.ID, attachment, encReader, sigReader)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to create attachment") return "", errors.Wrap(err, "failed to create attachment")
} }
@ -155,19 +170,25 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
return draft.ID, nil return draft.ID, nil
} }
func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress, msg Message) { func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress, msg Message, preparedImportRequestsCh chan map[string]*pmapi.ImportMsgReq) {
importMsgReq, err := p.generateImportMsgReq(msg, rules.globalMailbox) importMsgReq, err := p.generateImportMsgReq(msg, rules.globalMailbox)
if err != nil { if err != nil {
progress.messageImported(msg.ID, "", err) progress.messageImported(msg.ID, "", err)
return return
} }
importMsgReqSize := len(importMsgReq.Body) if progress.shouldStop() {
if p.importMsgReqSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.importMsgReqMap) == pmapiImportBatchMaxItems { return
p.importMessages(progress)
} }
p.importMsgReqMap[msg.ID] = importMsgReq
p.importMsgReqSize += importMsgReqSize importMsgReqSize := len(importMsgReq.Body)
if p.nextImportRequestsSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.nextImportRequests) == pmapiImportBatchMaxItems {
preparedImportRequestsCh <- p.nextImportRequests
p.nextImportRequests = map[string]*pmapi.ImportMsgReq{}
p.nextImportRequestsSize = 0
}
p.nextImportRequests[msg.ID] = importMsgReq
p.nextImportRequestsSize += importMsgReqSize
} }
func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox) (*pmapi.ImportMsgReq, error) { func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox) (*pmapi.ImportMsgReq, error) {
@ -176,7 +197,9 @@ func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox
return nil, errors.Wrap(err, "failed to parse message") return nil, errors.Wrap(err, "failed to parse message")
} }
p.timeIt.start("encrypt", msg.ID)
body, err := p.encryptMessage(message, attachmentReaders) body, err := p.encryptMessage(message, attachmentReaders)
p.timeIt.stop("encrypt", msg.ID)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to encrypt message") return nil, errors.Wrap(err, "failed to encrypt message")
} }
@ -208,6 +231,9 @@ func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox
} }
func (p *PMAPIProvider) parseMessage(msg Message) (m *pmapi.Message, r []io.Reader, err error) { func (p *PMAPIProvider) parseMessage(msg Message) (m *pmapi.Message, r []io.Reader, err error) {
p.timeIt.start("parse", msg.ID)
defer p.timeIt.stop("parse", msg.ID)
// Old message parser is panicking in some cases. // Old message parser is panicking in some cases.
// Instead of crashing we try to convert to regular error. // Instead of crashing we try to convert to regular error.
defer func() { defer func() {
@ -254,26 +280,39 @@ func computeMessageFlags(labels []string) (flag int64) {
return flag return flag
} }
func (p *PMAPIProvider) importMessages(progress *Progress) { func (p *PMAPIProvider) startImportWorkers(progress *Progress, preparedImportRequestsCh chan map[string]*pmapi.ImportMsgReq) *sync.WaitGroup {
var wg sync.WaitGroup
wg.Add(pmapiImportWorkers)
for i := 0; i < pmapiImportWorkers; i++ {
go func() {
for importRequests := range preparedImportRequestsCh {
p.importMessages(progress, importRequests)
}
wg.Done()
}()
}
return &wg
}
func (p *PMAPIProvider) importMessages(progress *Progress, importRequests map[string]*pmapi.ImportMsgReq) {
if progress.shouldStop() { if progress.shouldStop() {
return return
} }
importMsgIDs := []string{} importMsgIDs := []string{}
importMsgRequests := []*pmapi.ImportMsgReq{} importMsgRequests := []*pmapi.ImportMsgReq{}
for msgID, req := range p.importMsgReqMap { for msgID, req := range importRequests {
importMsgIDs = append(importMsgIDs, msgID) importMsgIDs = append(importMsgIDs, msgID)
importMsgRequests = append(importMsgRequests, req) importMsgRequests = append(importMsgRequests, req)
} }
log.WithField("msgIDs", importMsgIDs).Trace("Importing messages")
log.WithField("msgIDs", importMsgIDs).WithField("size", p.importMsgReqSize).Debug("Importing messages") results, err := p.importRequest(importMsgIDs[0], importMsgRequests)
results, err := p.importRequest(importMsgRequests)
// In case the whole request failed, try to import every message one by one. // In case the whole request failed, try to import every message one by one.
if err != nil || len(results) == 0 { if err != nil || len(results) == 0 {
log.WithError(err).Warning("Importing messages failed, trying one by one") log.WithError(err).Warning("Importing messages failed, trying one by one")
for msgID, req := range p.importMsgReqMap { for msgID, req := range importRequests {
importedID, err := p.importMessage(progress, req) importedID, err := p.importMessage(msgID, progress, req)
progress.messageImported(msgID, importedID, err) progress.messageImported(msgID, importedID, err)
} }
return return
@ -285,20 +324,17 @@ func (p *PMAPIProvider) importMessages(progress *Progress) {
if result.Error != nil { if result.Error != nil {
log.WithError(result.Error).WithField("msg", msgID).Warning("Importing message failed, trying alone") log.WithError(result.Error).WithField("msg", msgID).Warning("Importing message failed, trying alone")
req := importMsgRequests[index] req := importMsgRequests[index]
importedID, err := p.importMessage(progress, req) importedID, err := p.importMessage(msgID, progress, req)
progress.messageImported(msgID, importedID, err) progress.messageImported(msgID, importedID, err)
} else { } else {
progress.messageImported(msgID, result.MessageID, nil) progress.messageImported(msgID, result.MessageID, nil)
} }
} }
p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{}
p.importMsgReqSize = 0
} }
func (p *PMAPIProvider) importMessage(progress *Progress, req *pmapi.ImportMsgReq) (importedID string, importedErr error) { func (p *PMAPIProvider) importMessage(msgSourceID string, progress *Progress, req *pmapi.ImportMsgReq) (importedID string, importedErr error) {
progress.callWrap(func() error { progress.callWrap(func() error {
results, err := p.importRequest([]*pmapi.ImportMsgReq{req}) results, err := p.importRequest(msgSourceID, []*pmapi.ImportMsgReq{req})
if err != nil { if err != nil {
return errors.Wrap(err, "failed to import messages") return errors.Wrap(err, "failed to import messages")
} }

View File

@ -18,6 +18,7 @@
package transfer package transfer
import ( import (
"fmt"
"io" "io"
"time" "time"
@ -71,6 +72,11 @@ func (p *PMAPIProvider) tryReconnect() error {
func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*pmapi.Message, count int, err error) { func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*pmapi.Message, count int, err error) {
err = p.ensureConnection(func() error { err = p.ensureConnection(func() error {
// Sort is used in the key so the filter is different for estimating and real fetching.
key := fmt.Sprintf("%s_%s_%d", filter.LabelID, filter.Sort, filter.Page)
p.timeIt.start("listing", key)
defer p.timeIt.stop("listing", key)
messages, count, err = p.client().ListMessages(filter) messages, count, err = p.client().ListMessages(filter)
return err return err
}) })
@ -79,30 +85,44 @@ func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*
func (p *PMAPIProvider) getMessage(msgID string) (message *pmapi.Message, err error) { func (p *PMAPIProvider) getMessage(msgID string) (message *pmapi.Message, err error) {
err = p.ensureConnection(func() error { err = p.ensureConnection(func() error {
p.timeIt.start("download", msgID)
defer p.timeIt.stop("download", msgID)
message, err = p.client().GetMessage(msgID) message, err = p.client().GetMessage(msgID)
return err return err
}) })
return return
} }
func (p *PMAPIProvider) importRequest(req []*pmapi.ImportMsgReq) (res []*pmapi.ImportMsgRes, err error) { func (p *PMAPIProvider) importRequest(msgSourceID string, req []*pmapi.ImportMsgReq) (res []*pmapi.ImportMsgRes, err error) {
err = p.ensureConnection(func() error { err = p.ensureConnection(func() error {
p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID)
res, err = p.client().Import(req) res, err = p.client().Import(req)
return err return err
}) })
return return
} }
func (p *PMAPIProvider) createDraft(message *pmapi.Message, parent string, action int) (draft *pmapi.Message, err error) { func (p *PMAPIProvider) createDraft(msgSourceID string, message *pmapi.Message, parent string, action int) (draft *pmapi.Message, err error) {
err = p.ensureConnection(func() error { err = p.ensureConnection(func() error {
p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID)
draft, err = p.client().CreateDraft(message, parent, action) draft, err = p.client().CreateDraft(message, parent, action)
return err return err
}) })
return return
} }
func (p *PMAPIProvider) createAttachment(att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error) { func (p *PMAPIProvider) createAttachment(msgSourceID string, att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error) {
err = p.ensureConnection(func() error { err = p.ensureConnection(func() error {
// Use some attributes from attachment to have unique key for each call.
key := fmt.Sprintf("%s_%s_%d", msgSourceID, att.Name, att.Size)
p.timeIt.start("upload", key)
defer p.timeIt.stop("upload", key)
created, err = p.client().CreateAttachment(att, r, sig) created, err = p.client().CreateAttachment(att, r, sig)
return err return err
}) })

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,80 @@
// 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 (
"sync"
"time"
)
type timeIt struct {
lock sync.Locker
name string
groups map[string]int64
ongoing map[string]time.Time
}
func newTimeIt(name string) *timeIt {
return &timeIt{
lock: &sync.Mutex{},
name: name,
groups: map[string]int64{},
ongoing: map[string]time.Time{},
}
}
func (t *timeIt) clear() {
t.lock.Lock()
defer t.lock.Unlock()
t.groups = map[string]int64{}
t.ongoing = map[string]time.Time{}
}
func (t *timeIt) start(group, id string) {
t.lock.Lock()
defer t.lock.Unlock()
t.ongoing[group+"/"+id] = time.Now()
}
func (t *timeIt) stop(group, id string) {
endTime := time.Now()
t.lock.Lock()
defer t.lock.Unlock()
startTime, ok := t.ongoing[group+"/"+id]
if !ok {
log.WithField("group", group).WithField("id", id).Error("Stop called before start")
return
}
delete(t.ongoing, group+"/"+id)
diff := endTime.Sub(startTime).Milliseconds()
t.groups[group] += diff
}
func (t *timeIt) logResults() {
t.lock.Lock()
defer t.lock.Unlock()
// Print also ongoing to be sure that nothing was left out.
// Basically ongoing should be empty.
log.WithField("name", t.name).WithField("result", t.groups).WithField("ongoing", t.ongoing).Debug("Time measurement")
}

View File

@ -181,7 +181,10 @@ func (t *Transfer) Start() *Progress {
reportFile := newFileReport(t.logDir, t.id) reportFile := newFileReport(t.logDir, t.id)
progress := newProgress(log, reportFile) progress := newProgress(log, reportFile)
ch := make(chan Message) // Small queue to prevent having idle source while target is blocked.
// E.g., when upload to PM is in progress, we can in meantime download
// the next batch from remote IMAP server.
ch := make(chan Message, 10)
go func() { go func() {
defer t.panicHandler.HandlePanic() defer t.panicHandler.HandlePanic()

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,6 @@ package message
import ( import (
"mime" "mime"
"net/mail"
"net/textproto" "net/textproto"
"strings" "strings"
"time" "time"
@ -85,10 +84,6 @@ func GetHeader(msg *pmapi.Message) textproto.MIMEHeader { //nolint[funlen]
} }
if msg.ConversationID != "" { if msg.ConversationID != "" {
h.Set("X-Pm-ConversationID-Id", msg.ConversationID) h.Set("X-Pm-ConversationID-Id", msg.ConversationID)
if references := h.Get("References"); !strings.Contains(references, msg.ConversationID) {
references += " <" + msg.ConversationID + "@" + pmapi.ConversationIDDomain + ">"
h.Set("References", references)
}
} }
return h return h
@ -140,75 +135,3 @@ func GetAttachmentHeader(att *pmapi.Attachment) textproto.MIMEHeader {
return h return h
} }
// ========= Header parsing and sanitizing functions =========
func parseHeader(h mail.Header) (m *pmapi.Message, err error) { //nolint[unparam]
m = pmapi.NewMessage()
if subject, err := pmmime.DecodeHeader(h.Get("Subject")); err == nil {
m.Subject = subject
}
if addrs, err := sanitizeAddressList(h, "From"); err == nil && len(addrs) > 0 {
m.Sender = addrs[0]
}
if addrs, err := sanitizeAddressList(h, "Reply-To"); err == nil && len(addrs) > 0 {
m.ReplyTos = addrs
}
if addrs, err := sanitizeAddressList(h, "To"); err == nil {
m.ToList = addrs
}
if addrs, err := sanitizeAddressList(h, "Cc"); err == nil {
m.CCList = addrs
}
if addrs, err := sanitizeAddressList(h, "Bcc"); err == nil {
m.BCCList = addrs
}
m.Time = 0
if t, err := h.Date(); err == nil && !t.IsZero() {
m.Time = t.Unix()
}
m.Header = h
return
}
func sanitizeAddressList(h mail.Header, field string) (addrs []*mail.Address, err error) {
raw := h.Get(field)
if raw == "" {
err = mail.ErrHeaderNotPresent
return
}
var decoded string
decoded, err = pmmime.DecodeHeader(raw)
if err != nil {
return
}
addrs, err = mail.ParseAddressList(parseAddressComment(decoded))
if err == nil {
if addrs == nil {
addrs = []*mail.Address{}
}
return
}
// Probably missing encoding error -- try to at least parse addresses in brackets.
addrStr := h.Get(field)
first := strings.Index(addrStr, "<")
last := strings.LastIndex(addrStr, ">")
if first < 0 || last < 0 || first >= last {
return
}
var addrList []string
open := first
for open < last && 0 <= open {
addrStr = addrStr[open:]
close := strings.Index(addrStr, ">")
addrList = append(addrList, addrStr[:close+1])
addrStr = addrStr[close:]
open = strings.Index(addrStr, "<")
last = strings.LastIndex(addrStr, ">")
}
addrStr = strings.Join(addrList, ", ")
//
return mail.ParseAddressList(addrStr)
}

View File

@ -1,76 +0,0 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"errors"
escape "html"
"strings"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
func plaintextToHTML(text string) (output string) {
text = escape.EscapeString(text)
text = strings.Replace(text, "\n\r", "<br>", -1)
text = strings.Replace(text, "\r\n", "<br>", -1)
text = strings.Replace(text, "\n", "<br>", -1)
text = strings.Replace(text, "\r", "<br>", -1)
return "<div>" + text + "</div>"
}
func stripHTML(input string) (stripped string, err error) {
reader := strings.NewReader(input)
doc, _ := html.Parse(reader)
body := cascadia.MustCompile("body").MatchFirst(doc)
if body == nil {
err = errors.New("failed to find necessary html element")
return
}
var buf1 bytes.Buffer
if err = html.Render(&buf1, body); err != nil {
stripped = input
return
}
stripped = buf1.String()
// Handle double body tags edge case.
if strings.Index(stripped, "<body") == 0 {
startIndex := strings.Index(stripped, ">")
if startIndex < 5 {
return
}
stripped = stripped[startIndex+1:]
// Closing body tag is optional.
closingIndex := strings.Index(stripped, "</body>")
if closingIndex > -1 {
stripped = stripped[:closingIndex]
}
}
return
}
func addOuterHTMLTags(input string) (output string) {
return "<html><head></head><body>" + input + "</body></html>"
}
func makeEmbeddedImageHTML(cid, name string) (output string) {
return "<img class=\"proton-embedded\" alt=\"" + name + "\" src=\"cid:" + cid + "\">"
}

View File

@ -19,455 +19,506 @@ package message
import ( import (
"bytes" "bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"math/rand"
"mime" "mime"
"mime/quotedprintable"
"net/mail" "net/mail"
"net/textproto" "net/textproto"
"regexp"
"strconv"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/pkg/message/parser"
"github.com/ProtonMail/proton-bridge/pkg/message/rfc5322"
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime" pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-message"
"github.com/jaytaylor/html2text" "github.com/jaytaylor/html2text"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
) )
func parseAttachment(filename string, mediaType string, h textproto.MIMEHeader) (att *pmapi.Attachment) { func Parse(r io.Reader, key, keyName string) (m *pmapi.Message, mimeBody, plainBody string, attReaders []io.Reader, err error) {
if decoded, err := pmmime.DecodeHeader(filename); err == nil { logrus.Trace("Parsing message")
filename = decoded
} p, err := parser.New(r)
if filename == "" { if err != nil {
ext, err := mime.ExtensionsByType(mediaType) err = errors.Wrap(err, "failed to create new parser")
if err == nil && len(ext) > 0 { return
filename = "attachment" + ext[0]
}
} }
att = &pmapi.Attachment{ if err = convertForeignEncodings(p); err != nil {
Name: filename, err = errors.Wrap(err, "failed to convert foreign encodings")
MIMEType: mediaType, return
Header: h,
} }
headerContentID := strings.Trim(h.Get("Content-Id"), " <>") m = pmapi.NewMessage()
if headerContentID != "" { if err = parseMessageHeader(m, p.Root().Header); err != nil {
att.ContentID = headerContentID err = errors.Wrap(err, "failed to parse message header")
return
} }
return if m.Attachments, attReaders, err = collectAttachments(p); err != nil {
err = errors.Wrap(err, "failed to collect attachments")
return
}
if m.Body, plainBody, err = buildBodies(p); err != nil {
err = errors.Wrap(err, "failed to build bodies")
return
}
if m.MIMEType, err = determineMIMEType(p); err != nil {
err = errors.Wrap(err, "failed to determine mime type")
return
}
// We only attach the public key manually to the MIME body for
// signed/encrypted external recipients. It's not important for it to be
// collected as an attachment; that's already done when we upload the draft.
if key != "" {
attachPublicKey(p.Root(), key, keyName)
}
mimeBodyBuffer := new(bytes.Buffer)
if err = p.NewWriter().Write(mimeBodyBuffer); err != nil {
err = errors.Wrap(err, "failed to write out mime message")
return
}
return m, mimeBodyBuffer.String(), plainBody, attReaders, nil
} }
var reEmailComment = regexp.MustCompile("[(][^)]*[)]") //nolint[gochecknoglobals] func convertForeignEncodings(p *parser.Parser) error {
logrus.Trace("Converting foreign encodings")
// parseAddressComment removes the comments completely even though they should be allowed return p.NewWalker().
// http://tools.wordtothewise.com/rfc/822 RegisterContentTypeHandler("text/html", func(p *parser.Part) error {
// NOTE: This should be supported in go>1.10 but it seems it's not ¯\_(ツ)_/¯ if err := p.ConvertToUTF8(); err != nil {
func parseAddressComment(raw string) string { return err
return reEmailComment.ReplaceAllString(raw, "")
}
// Some clients incorrectly format messages with embedded attachments to have a format like
// I. text/plain II. attachment III. text/plain
// which we need to convert to a single HTML part with an embedded attachment.
func combineParts(m *pmapi.Message, parts []io.Reader, headers []textproto.MIMEHeader, convertPlainToHTML bool, atts *[]io.Reader) (isHTML bool, err error) { //nolint[funlen]
isHTML = true
foundText := false
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
h := headers[i]
disp, dispParams, _ := pmmime.ParseMediaType(h.Get("Content-Disposition"))
d := pmmime.DecodeContentEncoding(part, h.Get("Content-Transfer-Encoding"))
if d == nil {
log.Warnf("Unsupported Content-Transfer-Encoding '%v'", h.Get("Content-Transfer-Encoding"))
d = part
}
contentType := h.Get("Content-Type")
if contentType == "" {
contentType = "text/plain"
}
mediaType, params, _ := pmmime.ParseMediaType(contentType)
if strings.HasPrefix(mediaType, "text/") && mediaType != "text/calendar" && disp != "attachment" {
// This is text.
var b []byte
if b, err = ioutil.ReadAll(d); err != nil {
continue
} }
b, err = pmmime.DecodeCharset(b, contentType)
return p.ConvertMetaCharset()
}).
RegisterContentTypeHandler("text/.*", func(p *parser.Part) error {
return p.ConvertToUTF8()
}).
RegisterDefaultHandler(func(p *parser.Part) error {
t, params, _ := p.ContentType()
// multipart/alternative, for example, can contain extra charset.
if params != nil && params["charset"] != "" {
return p.ConvertToUTF8()
}
logrus.WithField("type", t).Trace("Not converting part to utf-8")
return nil
}).
Walk()
}
func collectAttachments(p *parser.Parser) ([]*pmapi.Attachment, []io.Reader, error) {
var (
atts []*pmapi.Attachment
data []io.Reader
err error
)
w := p.NewWalker().
RegisterContentDispositionHandler("attachment", func(p *parser.Part) error {
att, err := parseAttachment(p.Header)
if err != nil { if err != nil {
log.Warn("Decode charset error: ", err) return err
return false, err
}
contents := string(b)
if strings.Contains(mediaType, "text/plain") && len(contents) > 0 {
if !convertPlainToHTML {
isHTML = false
} else {
contents = plaintextToHTML(contents)
}
} else if strings.Contains(mediaType, "text/html") && len(contents) > 0 {
contents, err = stripHTML(contents)
if err != nil {
return isHTML, err
}
}
m.Body = contents + m.Body
foundText = true
} else {
// This is an attachment.
filename := dispParams["filename"]
if filename == "" {
// Using "name" in Content-Type is discouraged.
filename = params["name"]
}
if filename == "" && mediaType == "text/calendar" {
filename = "event.ics"
} }
att := parseAttachment(filename, mediaType, h) atts = append(atts, att)
data = append(data, bytes.NewReader(p.Body))
b := &bytes.Buffer{} return nil
if d == nil { }).
continue RegisterContentTypeHandler("text/calendar", func(p *parser.Part) error {
} att, err := parseAttachment(p.Header)
if _, err = io.Copy(b, d); err != nil { if err != nil {
continue return err
}
if foundText && att.ContentID == "" && strings.Contains(mediaType, "image") {
// Treat this as an inline attachment even though it is not marked as one.
hasher := sha256.New()
_, _ = hasher.Write([]byte(att.Name + strconv.Itoa(b.Len())))
bytes := hasher.Sum(nil)
cid := hex.EncodeToString(bytes) + "@protonmail.com"
att.ContentID = cid
embeddedHTML := makeEmbeddedImageHTML(cid, att.Name)
m.Body = embeddedHTML + m.Body
} }
m.Attachments = append(m.Attachments, att) atts = append(atts, att)
*atts = append(*atts, b) data = append(data, bytes.NewReader(p.Body))
}
}
if isHTML {
m.Body = addOuterHTMLTags(m.Body)
}
return isHTML, nil
}
func checkHeaders(headers []textproto.MIMEHeader) bool { return nil
foundAttachment := false }).
RegisterContentTypeHandler("text/.*", func(p *parser.Part) error {
return nil
}).
RegisterDefaultHandler(func(p *parser.Part) error {
if len(p.Children()) > 0 {
return nil
}
for i := 0; i < len(headers); i++ { att, err := parseAttachment(p.Header)
h := headers[i] if err != nil {
return err
}
mediaType, _, _ := pmmime.ParseMediaType(h.Get("Content-Type")) atts = append(atts, att)
data = append(data, bytes.NewReader(p.Body))
if !strings.HasPrefix(mediaType, "text/") { return nil
foundAttachment = true })
} else if foundAttachment {
// This means that there is a text part after the first attachment,
// so we will have to convert the body from plain->HTML.
return true
}
}
return false
}
// ============================== 7bit Filter ========================== if err = w.Walk(); err != nil {
// For every MIME part in the tree that has "8bit" or "binary" content return nil, nil, err
// transfer encoding: transcode it to "quoted-printable".
type SevenBitFilter struct {
target pmmime.VisitAcceptor
}
func NewSevenBitFilter(targetAccepter pmmime.VisitAcceptor) *SevenBitFilter {
return &SevenBitFilter{
target: targetAccepter,
}
}
func decodePart(partReader io.Reader, header textproto.MIMEHeader) (decodedPart io.Reader) {
decodedPart = pmmime.DecodeContentEncoding(partReader, header.Get("Content-Transfer-Encoding"))
if decodedPart == nil {
log.Warnf("Unsupported Content-Transfer-Encoding '%v'", header.Get("Content-Transfer-Encoding"))
decodedPart = partReader
}
return
}
func (sd SevenBitFilter) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) error {
cte := strings.ToLower(header.Get("Content-Transfer-Encoding"))
if isFirst && pmmime.IsLeaf(header) && cte != "quoted-printable" && cte != "base64" && cte != "7bit" {
decodedPart := decodePart(partReader, header)
filteredHeader := textproto.MIMEHeader{}
for k, v := range header {
filteredHeader[k] = v
}
filteredHeader.Set("Content-Transfer-Encoding", "quoted-printable")
filteredBuffer := &bytes.Buffer{}
decodedSlice, _ := ioutil.ReadAll(decodedPart)
w := quotedprintable.NewWriter(filteredBuffer)
if _, err := w.Write(decodedSlice); err != nil {
log.Errorf("cannot write quotedprintable from %q: %v", cte, err)
}
if err := w.Close(); err != nil {
log.Errorf("cannot close quotedprintable from %q: %v", cte, err)
}
_ = sd.target.Accept(filteredBuffer, filteredHeader, hasPlainSibling, true, isLast)
} else {
_ = sd.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
}
return nil
}
// =================== HTML Only convertor ==================================
// In any part of MIME tree structure, replace standalone text/html with
// multipart/alternative containing both text/html and text/plain.
type HTMLOnlyConvertor struct {
target pmmime.VisitAcceptor
}
func NewHTMLOnlyConvertor(targetAccepter pmmime.VisitAcceptor) *HTMLOnlyConvertor {
return &HTMLOnlyConvertor{
target: targetAccepter,
}
}
func randomBoundary() string {
buf := make([]byte, 30)
// We specifically use `math/rand` here to allow the generator to be seeded for test purposes.
// The random numbers need not be cryptographically secure; we are simply generating random part boundaries.
if _, err := rand.Read(buf); err != nil { // nolint[gosec]
panic(err)
} }
return fmt.Sprintf("%x", buf) return atts, data, nil
} }
func (hoc HTMLOnlyConvertor) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSiblings bool, isFirst, isLast bool) error { // buildBodies collects all text/html and text/plain parts and returns two bodies,
mediaType, _, err := pmmime.ParseMediaType(header.Get("Content-Type")) // - a rich text body (in which html is allowed), and
if isFirst && err == nil && mediaType == "text/html" && !hasPlainSiblings { // - a plaintext body (in which html is converted to plaintext).
multiPartHeaders := make(textproto.MIMEHeader) //
for k, v := range header { // text/html parts are converted to plaintext in order to build the plaintext body,
multiPartHeaders[k] = v // unless there is already a plaintext part provided via multipart/alternative,
} // in which case the provided alternative is chosen.
boundary := randomBoundary() func buildBodies(p *parser.Parser) (richBody, plainBody string, err error) {
multiPartHeaders.Set("Content-Type", "multipart/alternative; boundary=\""+boundary+"\"") richParts, err := collectBodyParts(p, "text/html")
childCte := header.Get("Content-Transfer-Encoding") if err != nil {
return
}
_ = hoc.target.Accept(partReader, multiPartHeaders, false, true, false) plainParts, err := collectBodyParts(p, "text/plain")
if err != nil {
return
}
partData, _ := ioutil.ReadAll(partReader) richBuilder, plainBuilder := strings.Builder{}, strings.Builder{}
htmlChildHeaders := make(textproto.MIMEHeader) for _, richPart := range richParts {
htmlChildHeaders.Set("Content-Transfer-Encoding", childCte) _, _ = richBuilder.Write(richPart.Body)
htmlChildHeaders.Set("Content-Type", "text/html") }
htmlReader := bytes.NewReader(partData)
_ = hoc.target.Accept(htmlReader, htmlChildHeaders, false, true, false)
_ = hoc.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, false) for _, plainPart := range plainParts {
_, _ = plainBuilder.Write(getPlainBody(plainPart))
}
plainChildHeaders := make(textproto.MIMEHeader) return richBuilder.String(), plainBuilder.String(), nil
plainChildHeaders.Set("Content-Transfer-Encoding", childCte) }
plainChildHeaders.Set("Content-Type", "text/plain")
unHtmlized, err := html2text.FromReader(bytes.NewReader(partData)) // collectBodyParts collects all body parts in the parse tree, preferring
// parts of the given content type if alternatives exist.
func collectBodyParts(p *parser.Parser, preferredContentType string) (parser.Parts, error) {
v := p.
NewVisitor(func(p *parser.Part, visit parser.Visit) (interface{}, error) {
childParts, err := collectChildParts(p, visit)
if err != nil {
return nil, err
}
return joinChildParts(childParts), nil
}).
RegisterRule("multipart/alternative", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
childParts, err := collectChildParts(p, visit)
if err != nil {
return nil, err
}
return bestChoice(childParts, preferredContentType), nil
}).
RegisterRule("text/plain", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
if disp == "attachment" {
return parser.Parts{}, nil
}
return parser.Parts{p}, nil
}).
RegisterRule("text/html", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
if disp == "attachment" {
return parser.Parts{}, nil
}
return parser.Parts{p}, nil
})
res, err := v.Visit()
if err != nil {
return nil, err
}
return res.(parser.Parts), nil
}
func collectChildParts(p *parser.Part, visit parser.Visit) ([]parser.Parts, error) {
childParts := []parser.Parts{}
for _, child := range p.Children() {
res, err := visit(child)
if err != nil { if err != nil {
unHtmlized = string(partData) return nil, err
} }
plainReader := strings.NewReader(unHtmlized)
_ = hoc.target.Accept(plainReader, plainChildHeaders, false, true, true)
_ = hoc.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, true) childParts = append(childParts, res.(parser.Parts))
} else {
_ = hoc.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
} }
return nil
return childParts, nil
} }
// ======= Public Key Attacher ======== func joinChildParts(childParts []parser.Parts) parser.Parts {
res := parser.Parts{}
type PublicKeyAttacher struct { for _, parts := range childParts {
target pmmime.VisitAcceptor res = append(res, parts...)
attachedPublicKey string }
attachedPublicKeyName string
appendToMultipart bool return res
depth int
} }
func NewPublicKeyAttacher(targetAccepter pmmime.VisitAcceptor, attachedPublicKey, attachedPublicKeyName string) *PublicKeyAttacher { func bestChoice(childParts []parser.Parts, preferredContentType string) parser.Parts {
return &PublicKeyAttacher{ // If one of the parts has preferred content type, use that.
target: targetAccepter, for i := len(childParts) - 1; i >= 0; i-- {
attachedPublicKey: attachedPublicKey, if allPartsHaveContentType(childParts[i], preferredContentType) {
attachedPublicKeyName: attachedPublicKeyName, return childParts[i]
appendToMultipart: false,
depth: 0,
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func split(input string, sliceLength int) string {
processed := input
result := ""
for len(processed) > 0 {
cutPoint := min(sliceLength, len(processed))
part := processed[0:cutPoint]
result = result + part + "\n"
processed = processed[cutPoint:]
}
return result
}
func createKeyAttachment(publicKey, publicKeyName string) (headers textproto.MIMEHeader, contents io.Reader) {
attachmentHeaders := make(textproto.MIMEHeader)
attachmentHeaders.Set("Content-Type", "application/pgp-key; name=\""+publicKeyName+"\"")
attachmentHeaders.Set("Content-Transfer-Encoding", "base64")
attachmentHeaders.Set("Content-Disposition", "attachment; filename=\""+publicKeyName+".asc.pgp\"")
buffer := &bytes.Buffer{}
w := base64.NewEncoder(base64.StdEncoding, buffer)
_, _ = w.Write([]byte(publicKey))
_ = w.Close()
return attachmentHeaders, strings.NewReader(split(buffer.String(), 73))
}
func (pka *PublicKeyAttacher) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSiblings bool, isFirst, isLast bool) error {
if isFirst && !pmmime.IsLeaf(header) {
pka.depth++
}
if isLast && !pmmime.IsLeaf(header) {
defer func() {
pka.depth--
}()
}
isRoot := (header.Get("From") != "")
// NOTE: This should also work for unspecified Content-Type (in which case us-ascii text/plain is assumed)!
mediaType, _, err := pmmime.ParseMediaType(header.Get("Content-Type"))
if isRoot && isFirst && err == nil && pka.attachedPublicKey != "" { //nolint[gocritic]
if strings.HasPrefix(mediaType, "multipart/mixed") {
pka.appendToMultipart = true
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
} else {
// Create two siblings with attachment in the case toplevel is not multipart/mixed.
multiPartHeaders := make(textproto.MIMEHeader)
for k, v := range header {
multiPartHeaders[k] = v
}
boundary := randomBoundary()
multiPartHeaders.Set("Content-Type", "multipart/mixed; boundary=\""+boundary+"\"")
multiPartHeaders.Del("Content-Transfer-Encoding")
_ = pka.target.Accept(partReader, multiPartHeaders, false, true, false)
originalHeader := make(textproto.MIMEHeader)
originalHeader.Set("Content-Type", header.Get("Content-Type"))
if header.Get("Content-Transfer-Encoding") != "" {
originalHeader.Set("Content-Transfer-Encoding", header.Get("Content-Transfer-Encoding"))
}
_ = pka.target.Accept(partReader, originalHeader, false, true, false)
_ = pka.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, false)
attachmentHeaders, attachmentReader := createKeyAttachment(pka.attachedPublicKey, pka.attachedPublicKeyName)
_ = pka.target.Accept(attachmentReader, attachmentHeaders, false, true, true)
_ = pka.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, true)
} }
} else if isLast && pka.depth == 1 && pka.attachedPublicKey != "" {
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, false)
attachmentHeaders, attachmentReader := createKeyAttachment(pka.attachedPublicKey, pka.attachedPublicKeyName)
_ = pka.target.Accept(attachmentReader, attachmentHeaders, hasPlainSiblings, true, true)
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, true)
} else {
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
} }
return nil
// Otherwise, choose the last one.
return childParts[len(childParts)-1]
} }
// ======= Parser ========== func allPartsHaveContentType(parts parser.Parts, contentType string) bool {
if len(parts) == 0 {
func Parse(r io.Reader, attachedPublicKey, attachedPublicKeyName string) (m *pmapi.Message, mimeBody string, plainContents string, atts []io.Reader, err error) { return false
secondReader := new(bytes.Buffer)
_, _ = secondReader.ReadFrom(r)
mimeBody = secondReader.String()
mm, err := mail.ReadMessage(secondReader)
if err != nil {
return
} }
if m, err = parseHeader(mm.Header); err != nil { for _, part := range parts {
return t, _, err := part.ContentType()
if err != nil {
return false
}
if t != contentType {
return false
}
} }
h := textproto.MIMEHeader(m.Header) return true
mmBodyData, err := ioutil.ReadAll(mm.Body) }
if err != nil {
return func determineMIMEType(p *parser.Parser) (string, error) {
var isHTML bool
w := p.NewWalker().
RegisterContentTypeHandler("text/html", func(p *parser.Part) (err error) {
isHTML = true
return
})
if err := w.Walk(); err != nil {
return "", err
} }
printAccepter := pmmime.NewMIMEPrinter()
publicKeyAttacher := NewPublicKeyAttacher(printAccepter, attachedPublicKey, attachedPublicKeyName)
sevenBitFilter := NewSevenBitFilter(publicKeyAttacher)
plainTextCollector := pmmime.NewPlainTextCollector(sevenBitFilter)
htmlOnlyConvertor := NewHTMLOnlyConvertor(plainTextCollector)
visitor := pmmime.NewMimeVisitor(htmlOnlyConvertor)
err = pmmime.VisitAll(bytes.NewReader(mmBodyData), h, visitor)
/*
err = visitor.VisitAll(h, bytes.NewReader(mmBodyData))
*/
if err != nil {
return
}
mimeBody = printAccepter.String()
plainContents = plainTextCollector.GetPlainText()
parts, headers, err := pmmime.GetAllChildParts(bytes.NewReader(mmBodyData), h)
if err != nil {
return
}
convertPlainToHTML := checkHeaders(headers)
isHTML, err := combineParts(m, parts, headers, convertPlainToHTML, &atts)
if isHTML { if isHTML {
m.MIMEType = "text/html" return "text/html", nil
} else {
m.MIMEType = "text/plain"
} }
return m, mimeBody, plainContents, atts, err return "text/plain", nil
}
// getPlainBody returns the body of the given part, converting html to
// plaintext where possible.
func getPlainBody(part *parser.Part) []byte {
contentType, _, err := part.ContentType()
if err != nil {
return part.Body
}
switch contentType {
case "text/html":
text, err := html2text.FromReader(bytes.NewReader(part.Body))
if err != nil {
return part.Body
}
return []byte(text)
default:
return part.Body
}
}
func attachPublicKey(p *parser.Part, key, keyName string) {
h := message.Header{}
h.Set("Content-Type", fmt.Sprintf(`application/pgp-keys; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
h.Set("Content-Disposition", fmt.Sprintf(`attachment; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
h.Set("Content-Transfer-Encoding", "base64")
p.AddChild(&parser.Part{
Header: h,
Body: []byte(key),
})
}
func parseMessageHeader(m *pmapi.Message, h message.Header) error { // nolint[funlen]
mimeHeader, err := toMailHeader(h)
if err != nil {
return err
}
m.Header = mimeHeader
fields := h.Fields()
for fields.Next() {
switch strings.ToLower(fields.Key()) {
case "subject":
s, err := fields.Text()
if err != nil {
if s, err = pmmime.DecodeHeader(fields.Value()); err != nil {
return errors.Wrap(err, "failed to parse subject")
}
}
m.Subject = s
case "from":
sender, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse from")
}
if len(sender) > 0 {
m.Sender = sender[0]
}
case "to":
toList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse to")
}
m.ToList = toList
case "reply-to":
replyTos, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse reply-to")
}
m.ReplyTos = replyTos
case "cc":
ccList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse cc")
}
m.CCList = ccList
case "bcc":
bccList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse bcc")
}
m.BCCList = bccList
case "date":
date, err := rfc5322.ParseDateTime(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse date")
}
m.Time = date.Unix()
}
}
return nil
}
func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
att := &pmapi.Attachment{}
mimeHeader, err := toMIMEHeader(h)
if err != nil {
return nil, err
}
att.Header = mimeHeader
mimeType, _, err := h.ContentType()
if err != nil {
return nil, err
}
att.MIMEType = mimeType
_, dispParams, dispErr := h.ContentDisposition()
if dispErr != nil {
ext, err := mime.ExtensionsByType(att.MIMEType)
if err != nil {
return nil, err
}
if len(ext) > 0 {
att.Name = "attachment" + ext[0]
}
} else {
att.Name = dispParams["filename"]
if att.Name == "" {
att.Name = "attachment.bin"
}
}
att.ContentID = strings.Trim(h.Get("Content-Id"), " <>")
return att, nil
}
func toMailHeader(h message.Header) (mail.Header, error) {
mimeHeader := make(mail.Header)
if err := forEachDecodedHeaderField(h, func(key, val string) error {
mimeHeader[key] = []string{val}
return nil
}); err != nil {
return nil, err
}
return mimeHeader, nil
}
func toMIMEHeader(h message.Header) (textproto.MIMEHeader, error) {
mimeHeader := make(textproto.MIMEHeader)
if err := forEachDecodedHeaderField(h, func(key, val string) error {
mimeHeader[key] = []string{val}
return nil
}); err != nil {
return nil, err
}
return mimeHeader, nil
}
func forEachDecodedHeaderField(h message.Header, fn func(string, string) error) error {
fields := h.Fields()
for fields.Next() {
text, err := fields.Text()
if err != nil {
if !message.IsUnknownCharset(err) {
return err
}
if text, err = pmmime.DecodeHeader(fields.Value()); err != nil {
return err
}
}
if err := fn(fields.Key(), text); err != nil {
return err
}
}
return nil
} }

View File

@ -0,0 +1,59 @@
// 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 parser
import (
"regexp"
)
type HandlerFunc func(*Part) error
type handler struct {
typeRegExp, dispRegExp *regexp.Regexp
fn HandlerFunc
}
func (h *handler) matchPart(p *Part) bool {
return h.matchType(p) || h.matchDisp(p)
}
func (h *handler) matchType(p *Part) bool {
if h.typeRegExp == nil {
return false
}
t, _, err := p.ContentType()
if err != nil {
t = ""
}
return h.typeRegExp.MatchString(t)
}
func (h *handler) matchDisp(p *Part) bool {
if h.dispRegExp == nil {
return false
}
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
return h.dispRegExp.MatchString(disp)
}

View File

@ -0,0 +1,154 @@
// 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 parser
import (
"io"
"io/ioutil"
"github.com/emersion/go-message"
)
type Parser struct {
stack []*Part
root *Part
}
func New(r io.Reader) (*Parser, error) {
p := new(Parser)
entity, err := message.Read(newEndOfMailTrimmer(r))
if err != nil && !message.IsUnknownCharset(err) {
return nil, err
}
if err := p.parseEntity(entity); err != nil {
return nil, err
}
return p, nil
}
func (p *Parser) NewWalker() *Walker {
return newWalker(p.root)
}
func (p *Parser) NewVisitor(defaultRule VisitorRule) *Visitor {
return newVisitor(p.root, defaultRule)
}
func (p *Parser) NewWriter() *Writer {
return newWriter(p.root)
}
func (p *Parser) Root() *Part {
return p.root
}
// Section returns the message part referred to by the given section. A section
// is zero or more integers. For example, section 1.2.3 will return the third
// part of the second part of the first part of the message.
func (p *Parser) Section(section []int) (part *Part, err error) {
part = p.root
for _, n := range section {
if part, err = part.Child(n); err != nil {
return
}
}
return
}
func (p *Parser) beginPart() {
p.stack = append(p.stack, &Part{})
}
func (p *Parser) endPart() {
var part *Part
p.stack, part = p.stack[:len(p.stack)-1], p.stack[len(p.stack)-1]
if len(p.stack) > 0 {
p.top().children = append(p.top().children, part)
} else {
p.root = part
}
}
func (p *Parser) top() *Part {
if len(p.stack) == 0 {
return nil
}
return p.stack[len(p.stack)-1]
}
func (p *Parser) withHeader(h message.Header) {
p.top().Header = h
}
func (p *Parser) withBody(bytes []byte) {
p.top().Body = bytes
}
func (p *Parser) parseEntity(e *message.Entity) error {
p.beginPart()
defer p.endPart()
p.withHeader(e.Header)
if mr := e.MultipartReader(); mr != nil {
return p.parseMultipart(mr)
}
return p.parsePart(e)
}
func (p *Parser) parsePart(e *message.Entity) (err error) {
bytes, err := ioutil.ReadAll(e.Body)
if err != nil {
return
}
p.withBody(bytes)
return
}
func (p *Parser) parseMultipart(r message.MultipartReader) (err error) {
for {
var child *message.Entity
if child, err = r.NextPart(); err != nil && !message.IsUnknownCharset(err) {
return ignoreEOF(err)
}
if err = p.parseEntity(child); err != nil {
return
}
}
}
func ignoreEOF(err error) error {
if err == io.EOF {
return nil
}
return err
}

View File

@ -0,0 +1,54 @@
// 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 parser
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func newTestParser(t *testing.T, msg string) *Parser {
p, err := New(getFileReader(msg))
require.NoError(t, err)
return p
}
func getFileReader(filename string) io.ReadCloser {
f, err := os.Open(filepath.Join("testdata", filename))
if err != nil {
panic(err)
}
return f
}
func getFileAsString(filename string) string {
b, err := ioutil.ReadAll(getFileReader(filename))
if err != nil {
panic(err)
}
return string(b)
}

200
pkg/message/parser/part.go Normal file
View File

@ -0,0 +1,200 @@
// 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 parser
import (
"bytes"
"errors"
"mime"
"unicode/utf8"
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
"github.com/PuerkitoBio/goquery"
"github.com/emersion/go-message"
"github.com/sirupsen/logrus"
"golang.org/x/net/html"
"golang.org/x/net/html/charset"
"golang.org/x/text/encoding"
)
type Parts []*Part
type Part struct {
Header message.Header
Body []byte
children Parts
}
func (p *Part) ContentType() (string, map[string]string, error) {
t, params, err := p.Header.ContentType()
if err != nil {
// go-message's implementation of ContentType() doesn't handle duplicate parameters
// e.g. Content-Type: text/plain; charset=utf-8; charset=UTF-8
// so if it fails, we try again with pmmime's implementation, which does.
t, params, err = pmmime.ParseMediaType(p.Header.Get("Content-Type"))
}
return t, params, err
}
func (p *Part) Child(n int) (part *Part, err error) {
if len(p.children) < n {
return nil, errors.New("no such part")
}
return p.children[n-1], nil
}
func (p *Part) Children() Parts {
return p.children
}
func (p *Part) AddChild(child *Part) {
if p.isMultipartMixed() {
p.children = append(p.children, child)
} else {
root := &Part{
Header: getContentHeaders(p.Header),
Body: p.Body,
children: p.children,
}
p.Body = nil
p.children = Parts{root, child}
stripContentHeaders(&p.Header)
p.Header.Set("Content-Type", "multipart/mixed")
}
}
func (p *Part) ConvertToUTF8() error {
logrus.Trace("Converting part to utf-8")
t, params, err := p.ContentType()
if err != nil {
return err
}
decoder := selectSuitableDecoder(p, t, params)
if p.Body, err = decoder.Bytes(p.Body); err != nil {
return err
}
if params == nil {
params = make(map[string]string)
}
params["charset"] = "UTF-8"
p.Header.SetContentType(t, params)
return nil
}
func (p *Part) ConvertMetaCharset() error {
doc, err := html.Parse(bytes.NewReader(p.Body))
if err != nil {
return err
}
goquery.NewDocumentFromNode(doc).Find("meta").Each(func(n int, sel *goquery.Selection) {
if val, ok := sel.Attr("content"); ok {
t, params, err := pmmime.ParseMediaType(val)
if err != nil {
return
}
params["charset"] = "UTF-8"
sel.SetAttr("content", mime.FormatMediaType(t, params))
}
if _, ok := sel.Attr("charset"); ok {
sel.SetAttr("charset", "UTF-8")
}
})
buf := new(bytes.Buffer)
if err := html.Render(buf, doc); err != nil {
return err
}
p.Body = buf.Bytes()
return nil
}
func selectSuitableDecoder(p *Part, t string, params map[string]string) *encoding.Decoder {
if charset, ok := params["charset"]; ok {
logrus.WithField("charset", charset).Trace("The part has a specified charset")
if decoder, err := pmmime.SelectDecoder(charset); err == nil {
logrus.Trace("The charset is known; decoder has been selected")
return decoder
}
logrus.Warn("The charset is unknown; no decoder could be selected")
}
if utf8.Valid(p.Body) {
logrus.Trace("The part is already valid utf-8, returning noop encoder")
return encoding.Nop.NewDecoder()
}
encoding, name, _ := charset.DetermineEncoding(p.Body, t)
logrus.WithField("name", name).Warn("Determined encoding by reading body")
return encoding.NewDecoder()
}
func (p *Part) is7BitClean() bool {
for _, b := range p.Body {
if b > 1<<7 {
return false
}
}
return true
}
func (p *Part) isMultipartMixed() bool {
t, _, err := p.ContentType()
if err != nil {
return false
}
return t == "multipart/mixed"
}
func getContentHeaders(header message.Header) message.Header {
var res message.Header
res.Set("Content-Type", header.Get("Content-Type"))
res.Set("Content-Disposition", header.Get("Content-Disposition"))
res.Set("Content-Transfer-Encoding", header.Get("Content-Transfer-Encoding"))
return res
}
func stripContentHeaders(header *message.Header) {
header.Del("Content-Type")
header.Del("Content-Disposition")
header.Del("Content-Transfer-Encoding")
}

View File

@ -0,0 +1,73 @@
// 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 parser
import (
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPart(t *testing.T) {
p := newTestParser(t, "complex_structure.eml")
wantParts := map[string]string{
"": "multipart/mixed",
"1": "text/plain",
"2": "application/octet-stream",
"3": "multipart/mixed",
"3.1": "text/plain",
"3.2": "application/octet-stream",
"4": "multipart/mixed",
"4.1": "image/gif",
"4.2": "multipart/mixed",
"4.2.1": "text/plain",
"4.2.2": "multipart/alternative",
"4.2.2.1": "text/plain",
"4.2.2.2": "text/html",
}
for partNumber, wantContType := range wantParts {
part, err := p.Section(getSectionNumber(partNumber))
require.NoError(t, err)
contType, _, err := part.ContentType()
require.NoError(t, err)
assert.Equal(t, wantContType, contType)
}
}
func getSectionNumber(s string) (part []int) {
if s == "" {
return
}
for _, number := range strings.Split(s, ".") {
i64, err := strconv.ParseInt(number, 10, 64)
if err != nil {
panic(err)
}
part = append(part, int(i64))
}
return
}

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