Compare commits

..

164 Commits

Author SHA1 Message Date
6230200218 Import-Export app Elbe 1.2.2
Changed
* Improvements to the import from large mbox files with multiple labels
* Not allow to run multiple instances of the app or transfers at the same time
* Better handling and displaying of skipped messages
* Various enhancements of the import process related to parsing
* Cosmetic GUI changes
* Better error handling

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

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

1
.gitignore vendored
View File

@ -29,6 +29,7 @@ frontend/qml/*.qmlc
# Build files
bridge_darwin_*.tgz
cmd/Desktop-Bridge/deploy
cmd/Import-Export/deploy
internal/frontend/qt*/moc.cpp
internal/frontend/qt*/moc.go
internal/frontend/qt*/moc.h

View File

@ -82,7 +82,9 @@ dependency-updates:
script:
- make build
artifacts:
expire_in: 2 week
# Note: The latest artifacts for refs are locked against deletion, and kept regardless of the expiry time.
# Introduced in GitLab 13.0 behind a disabled feature flag, and made the default behavior in GitLab 13.4.
expire_in: 1 day
build-linux:
extends: .build-base
@ -91,6 +93,17 @@ build-linux:
paths:
- bridge_*.tgz
build-linux-qa:
extends: .build-base
only:
- web
script:
- BUILD_TAGS="build_qa pmapi_qa" make build
artifacts:
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-ie-linux:
extends: .build-base
script:
@ -124,6 +137,17 @@ build-darwin:
paths:
- bridge_*.tgz
build-darwin-qa:
extends: .build-darwin-base
only:
- web
script:
- BUILD_TAGS="build_qa pmapi_qa" make build
artifacts:
name: "bridge-darwin-qa-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-ie-darwin:
extends: .build-darwin-base
script:
@ -155,6 +179,23 @@ build-windows:
paths:
- bridge_*.tgz
build-windows-qa:
extends: .build-windows-base
only:
- web
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows BUILD_TAGS="build_qa pmapi_qa" make build
artifacts:
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-ie-windows:
extends: .build-windows-base
script:

View File

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

View File

@ -1,15 +1,154 @@
# ProtonMail Bridge Changelog
# ProtonMail Bridge and Import-Export app Changelog
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Unreleased
## [IE 1.2.1, 1.2.2] Elbe
## [IE 0.2.x] Congo
### Added
* GODT-799 Skipped messages do not change total counts but shows as separate number.
## Fixed
* GODT-799 Fix skipping unwanted folders importing from mbox files.
* GODT-769 Close connection before deleting labels to prevent panics accessing deleted bucket.
### Removed
* GODT-766 Remove GUI popup for IMAP TLS error.
## [Bridge 1.5.0] Golden Gate
### Changed
* Updated go-mbox dependency back to upstream.
### Fixed
* GODT-847 Waiting for unilateral update during deleting the message.
* GODT-849 Show in error counts in the end also lost messages.
* GODT-835 Do not include conversation ID in references to show properly conversation threads in clients.
* GODT-685 Improve deb packaging regarding dejavu font
## [IE 1.2.0] Elbe
### Added
* GODT-763 Detect Gmail labels from All Mail mbox export (using X-Gmail-Label header).
* GODT-834 Info about tags in BUILDS.md and link to Import-Export page in README.md.
* GODT-777 Support Apple Mail MBOX export format.
### Fixed
* GODT-677 Windows IE: global import settings not fit in window.
* GODT-794 Congo fails to update to Danube
* GODT-749 Don't force PGP/Inline when sending plaintext messages.
* GODT-764 Fix deadlock in integration tests for Import-Export.
* GODT-662 Do not resume paused transfer progress after dismissing cancel popup.
* GODT-772 Sanitize mailbox names for exporting to follow OS restrictions.
* GODT-771 Show fatal errors after export is terminated.
* GODT-779 Do not propagate updates when progress is stopped.
* GODT-779 Unpause progress during fatal error to properly stop progress.
* GODT-779 Stop ongoing transfer calls sooner (re-check after import request is generated).
* Fix measurement of uploading attachments during transfer.
* GODT-827 Do not spam sentry with bad ID by integration test.
* GODT-700 Fix UTF-7 incompatibility.
* GODT-837 Fix flaky TestFailUnpauseAndStops.
* GODT-782 Don't use TLS pinning when checking connectivity status.
### Changed
* TLS pins conform to official list.
## [Bridge 1.4.5] Forth
### Fixed
* GODT-829 Remove `NoInferior` to display sub-folders in apple mail.
## [Bridge 1.4.4] Forth
### Fixed
* GODT-798 Replace, don't add, transfer encoding when making body 7-bit clean.
* Move/Copy duplicate for emails with References in Outlook
* CSB-247 Cannot update from 1.4.0
## [Bridge 1.4.3] Forth
### Changed
* Reverted sending IMAP updates to be not blocking again.
### Fixed
* GODT-783 Settings flags by FLAGS (not using +/-FLAGS) do not change spam state.
## [Bridge 1.4.2] Forth
### Changed
* GODT-761 Use label.Path instead of Name to partially support subfolders for webapp beta release.
* GODT-765 Improve speed of checking whether message is deleted.
## [IE 1.1.2] Danube (beta 2020-09-xx)
### Fixed
* GODT-770 Better handling of extraneous end-of-mail indicator.
* GODT-776 Fix crash when IMAP client connects while account is logging in.
### Changed
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8
* GODT-785 Clear separation of different message IDs in integration tests.
### Changed
* GODT-741 Import-Export shows "Unable to parse time" notice instead of zero time in error report window.
* Bump crypto version to v0.0.0-20200818122824-ed5d25e28db8.
* GODT-374 Allow to send calendar update multiple times.
## [IE 1.1.1] Danube (beta 2020-09-xx) [Bridge 1.4.1] Forth (beta 2020-09-xx)
### Fixed
* GODT-752 Parsing message with empty addresses.
* GODT-752 Parsing non-utf8 multipart/alternative message.
* GODT-752 Parsing message with duplicate charset parameter.
## [IE 1.1.0] Danube
### Fixed
* GODT-703 Import-Export showed always at least one total message.
* GODT-738 Fix for mbox files with long lines.
### Fixed
* GODT-732 Do not mix font awesome icon with regular text to avoid issues on Fedora.
## [Bridge 1.4.0] Forth
### Added
* GODT-682 Persistent anonymous API cookies for Import-Export.
* GODT-357 Use go-message to make a better message parser.
* GODT-720 Time measurement of progress for Import-Export.
### Changed
* GODT-511 User agent format changed.
* Unsilent errors reading mbox files.
* GODT-692 QA build with option to change API URL by ENV variable.
* GODT-704 User agent detected by fake IMAP extension instead of AUTH callback (some clients use LOGIN instead of AUTH).
* GODT-695 Parallel upload for ProtonMail target.
### Removed
* GODT-519 Unused AUTH scope parsing methods.
### Fixed
* GODT-698 Use correct package type for signed PGP/Inline messages.
* Generic bug report window title.
* Fix missing check for unencrypted recipients during sending.
* Version checking for catalina.
* GODT-730 Limit maximal TLS version for Yahoo IMAP server.
## [IE 1.0.x] Congo (v1.0.0 live 2020-09-08)
### Added
* GODT-633 Persistent anonymous API cookies for better load balancing and abuse detection.
* GODT-461 Add support for `\Deleted` flag.
### Changed
* GODT-462 Pausing event loop while FETCHing to prevent EXPUNGE
* Wait for unilateral response to be delivered
* GODT-409 Set flags have to replace all flags.
* GODT-531 Better way to add trusted certificate in macOS.
* Bumped golangci-lint to v1.29.0
@ -36,6 +175,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* golang.org/x/text v0.3.2 -> v0.3.3
* Set first-start to false in bridge, not in frontend.
* GODT-400 Refactor sendingInfo.
* GODT-513 Update routes to API v4.
* GODT-551 Do not ignore errors during message flagging.
* GODT-380 Adding IE GUI to Bridge repo and building
* BR: extend functionality of PopupDialog
* BR: makefile APP_VERSION instead of BRIDGE_VERSION
@ -49,11 +190,14 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* IE: Removed `onLoginFinished`
* Structure for transfer rules in QML
* GODT-213 Convert panics from message parser to error.
* GODT-585 Do not allow deleting messages from All Mail.
### Fixed
* GODT-655 Fix date picker with automatic Windows DST
* GODT-454 Fix send on closed channel when receiving unencrypted send confirmation from GUI.
* 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)

View File

@ -9,8 +9,9 @@ TARGET_OS?=${GOOS}
## Build
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
BRIDGE_APP_VERSION?=1.4.0-git
IE_APP_VERSION?=1.0.0-git
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.5.0-git
IE_APP_VERSION?=1.2.2-git
APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns
@ -56,7 +57,6 @@ ifeq "${TARGET_CMD}" "Import-Export"
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
endif
build: ${TGZ_TARGET}
build-ie:
TARGET_CMD=Import-Export $(MAKE) build
@ -264,7 +264,6 @@ run-ie-qt:
run-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) run-nogui
clean-frontend-qt:
$(MAKE) -C internal/frontend/qt -f Makefile.local clean
clean-frontend-qt-ie:

View File

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

View File

@ -21,9 +21,11 @@ import (
"runtime/pprof"
"github.com/ProtonMail/proton-bridge/internal/cmd"
"github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/config"
@ -36,6 +38,10 @@ import (
)
const (
// cacheVersion is used for cache files such as lock, or preferences.
// Different number will drop old files and create new ones.
cacheVersion = "c11"
appName = "importExport"
appNameDash = "import-export-app"
)
@ -58,7 +64,7 @@ func main() {
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(appName, constants.Version, constants.Revision, "")
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
// We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash
@ -132,6 +138,16 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
pref := preferences.New(cfg)
// Cookies must be persisted across restarts.
jar, err := cookies.NewCookieJar(pref)
if err != nil {
logrus.WithError(err).Warn("Could not create cookie jar")
} else {
cm.SetCookieJar(jar)
}
importexportInstance := importexport.New(cfg, panicHandler, eventListener, cm, credentialsStore)
// Decide about frontend mode before initializing rest of import-export.

20
go.mod
View File

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

110
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/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
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/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8 h1:u1j0xLTrCHpNS40B6m4Sv3IVUz5m9jt+AnTIopT3IgM=
github.com/ProtonMail/crypto v0.0.0-20200818122824-ed5d25e28db8/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
@ -13,35 +15,38 @@ github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474 h1:D0RwDtkBw0Gt7hmbb1ivdEulplJAwu1i2jzh4HM45fo=
github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-rfc5322 v0.2.0 h1:tndoDGFtiCvESta9KLUeMksojz8qf76PefnkoQ+fqeg=
github.com/ProtonMail/go-rfc5322 v0.2.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
github.com/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/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
github.com/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/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -64,11 +69,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-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.0 h1:HN6aKbyqmgIfK9fS/gen+NRr2wXLSxZXWfdAIAnzQPc=
github.com/emersion/go-mbox v1.0.0/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac=
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a h1:3C6qIGgPr1qAT0ikRD5NbyKpME/iHCDeXhpv/JJsFsE=
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1:kYIioST9GDHte9/BRWgi93rpqbDuFftMjKSMaXS8ABo=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
@ -84,25 +89,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/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
github.com/go-delve/delve v1.4.1 h1:kZs0umEv+VKnK84kY9/ZXWrakdLTeRTyYjFdgLelZCQ=
github.com/go-delve/delve v1.4.1/go.mod h1:vmy6iObn7zg8FQ5KOCIe6TruMNsqpoZO8uMiRea+97k=
github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
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/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/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-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -110,88 +102,56 @@ 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/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/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/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/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/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
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.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.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/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
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/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/psampaz/go-mod-outdated v0.6.0 h1:DXS6rdsz4rpezbPsckQflqrYSEBvsF5GAmUWP+UvnQo=
github.com/psampaz/go-mod-outdated v0.6.0/go.mod h1:r78NYWd1z+F9Zdsfy70svgXOz363B08BWnTyFSgEESs=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -201,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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200603231648-26cdb75b6f22 h1:UrNr8EZueA1eREFmG5gVHBeeOuwW2GbzI9VfdB5uK+c=
github.com/therecipe/qt/internal/binding/files/docs v0.0.0-20191019224306-1097424d656c h1:/VhcwU7WuFEVgDHZ9V8PIYAyYqQ6KNxFUjBMOf2aFZM=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22 h1:FumuOkCw78iheUI3eIYhAgtsj/0HQBAib/jXk1cslJw=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22 h1:aYzTBQ/hC6FtbaRnyylxlhbSGMPnyD5lAzVO3Ae6emA=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
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/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-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-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-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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@ -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-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

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

View File

@ -15,17 +15,17 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Fri 04 Sep 2020 01:57:36 PM CEST'. DO NOT EDIT.
// Code generated by ./release-notes.sh at 'Wed Nov 4 12:24:35 PM CET 2020'. DO NOT EDIT.
package bridge
const ReleaseNotes = `Improvements to Alternative Routing: Version two of this feature is now more resilient to unstable internet connections, which results in a smoother experience using this feature. Also includes fixes to previous implementation of Alternative Routing when first starting the application or when turning it off.
Email parsing improvements: Improved detection of email encodings embedded in html/xml in addition to message header; add a fallback option if encoding is not specified and decoding as UTF8 fails (ISO-8859-1) ; tweaked logic of parsing "References" header.
User interaction improvements: Some smaller improvements in specific cases to make the interaction with Proton Bridge clearer for the user
Code updates & maintenance: Migrated to GopenPGP v2, updates to GoIMAPv1, increased bbolt version to 1.3.5 and various improvements regarding extensibility and maintainability for upcoming work.
• General stability improvements: Improvements to the behavior of the application under various unstable internet conditions.
const ReleaseNotes = `Ensured better message flow by refactoring both address and date parsing
Improved secure connectivity checks
Better deb packaging
More robust error handling
`
const ReleaseFixedBugs = `Fixed a slew of smaller bugs and some conditions which could cause the application to crash.
The full changelog can be found at https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
const ReleaseFixedBugs = `Ensured that conversations are properly threaded
Fixed Linux font issues (Fedora)
• Better handling of Mime encrypted messages
`

View File

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

View File

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

View File

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

View File

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

View File

@ -354,7 +354,7 @@ Window {
} else {
return qsTr('A new version of Bridge is available.<br>
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
You can continue with the update or download and install new version manually from<br><br>
You can continue with the update or download and install the new version manually from<br><br>
<a href="%3">%3</a>',
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -279,9 +279,8 @@ Dialog {
titleTo : root.address
}
Rectangle {
Column {
id: masterImportSettings
height: 150 // fixme
anchors {
right : parent.right
left : parent.left
@ -291,7 +290,11 @@ Dialog {
rightMargin : Style.main.leftMargin
bottomMargin : Style.main.bottomMargin
}
color: Style.dialog.background
spacing: Style.main.bottomMargin
Row {
spacing: masterImportSettings.width - labelMasterImportSettings.width - resetSourceButton.width
Text {
id: labelMasterImportSettings
@ -305,24 +308,21 @@ Dialog {
color: Style.main.text
InfoToolTip {
info: qsTr(
"If master import date range is selected only emails within this range will be imported, unless it is specified differently in folder date range.",
"Text in master import settings tooltip."
)
anchors {
left: parent.right
bottom: parent.bottom
leftMargin : Style.dialog.leftMargin
}
info: qsTr(
"If master import date range is selected only emails within this range will be imported, unless it is specified differently in folder date range.",
"Text in master import settings tooltip."
)
}
}
// Reset all to default
ClickIconText {
anchors {
right: parent.right
bottom: labelMasterImportSettings.bottom
}
id: resetSourceButton
text:qsTr("Reset all settings to default")
iconText: Style.fa.refresh
textColor: Style.main.textBlue
@ -332,6 +332,7 @@ Dialog {
timer.start()
}
}
}
Rectangle{
id: line
@ -348,30 +349,22 @@ Dialog {
InlineDateRange {
id: globalDateRange
anchors {
left : parent.left
top : line.bottom
topMargin : Style.dialog.topMargin
}
}
// Add global label (inline)
InlineLabelSelect {
id: globalLabels
anchors {
left : parent.left
top : globalDateRange.bottom
topMargin : Style.dialog.topMargin
}
//labelWidth : globalDateRange.labelWidth
}
// Buttons
Row {
spacing: Style.dialog.spacing
anchors {
bottom : parent.bottom
right: parent.right
bottom: parent.bottom
rightMargin: Style.main.leftMargin
bottomMargin: Style.main.bottomMargin
}
ButtonRounded {
@ -393,7 +386,6 @@ Dialog {
}
}
}
}
Rectangle { // Progress
id: progressStatus
@ -483,19 +475,31 @@ Dialog {
}
}
Text {
Row {
property int fails: go.progressFails
visible: fails > 0
color : Style.main.textRed
font.family: Style.fontawesome.name
font.pointSize: Style.main.fontSize * Style.pt
anchors.horizontalCenter: parent.horizontalCenter
text: Style.fa.exclamation_circle + " " + (
Text {
color: Style.main.textRed
font {
pointSize : Style.dialog.fontSize * Style.pt
family : Style.fontawesome.name
}
text: Style.fa.exclamation_circle
}
Text {
property int fails: go.progressFails
color: Style.main.textRed
font.pointSize: Style.main.fontSize * Style.pt
text: " " + (
fails == 1 ?
qsTr("%1 message failed to be imported").arg(fails) :
qsTr("%1 messages failed to be imported").arg(fails)
)
}
}
Row { // buttons
spacing: Style.dialog.rightMargin
@ -575,16 +579,27 @@ Dialog {
anchors.centerIn : finalReport
spacing : Style.dialog.heightSeparator
Text {
text: go.progressDescription!="" ? qsTr("Import failed: %1").arg(go.progressDescription) : Style.fa.check_circle + " " + qsTr("Import completed successfully")
Row {
anchors.horizontalCenter: parent.horizontalCenter
color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen
font.bold : true
font.family: Style.fontawesome.name
Text {
font {
pointSize: Style.dialog.fontSize * Style.pt
family: Style.fontawesome.name
}
color: Style.main.textGreen
text: go.progressDescription!="" ? "" : Style.fa.check_circle
}
Text {
text: qsTr("<b>Import summary:</b><br>Total number of emails: %1<br>Imported emails: %2<br>Errors: %3").arg(go.total).arg(finalReport.imported).arg(go.progressFails)
text: go.progressDescription!="" ? qsTr("Import failed: %1").arg(go.progressDescription) : " " + qsTr("Import completed successfully")
color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen
font.bold : true
}
}
Text {
text: qsTr("<b>Import summary:</b><br>Total number of emails: %1<br>Imported emails: %2<br>Filtered out emails: %3<br>Errors: %4").arg(go.total).arg(go.progressImported).arg(go.progressSkipped).arg(go.progressFails)
anchors.horizontalCenter: parent.horizontalCenter
textFormat: Text.RichText
horizontalAlignment: Text.AlignHCenter
@ -773,11 +788,6 @@ Dialog {
errorPopup.hide()
}
onClickedNo : {
if (errorPopup.msgID == "ask_send_report") {
errorPopup.hide()
return
}
go.resumeProcess()
errorPopup.hide()
}

View File

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

View File

@ -274,15 +274,15 @@ Window {
}
} else {
if (go.goos=="linux") {
return qsTr('New version of %1 is available.<br>
return qsTr('A new version of %1 is available.<br>
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
Use your package manager to update or download and install new version manually from<br><br>
Use your package manager to update or download and install new the version manually from<br><br>
<a href="%4">%4</a>',
"Message for update in Linux").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
} else {
return qsTr('New version of %1 is available.<br>
return qsTr('A new version of %1 is available.<br>
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
You can continue with update or download and install new version manually from<br><br>
You can continue with update or download and install new the version manually from<br><br>
<a href="%4">%4</a>',
"Message for update in Win/Mac").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
}

View File

@ -42,11 +42,35 @@ ComboBox {
root.below = popup.y>0
}
contentItem : Text {
contentItem : Row {
id: boxText
verticalAlignment: Text.AlignVCenter
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
}
@ -56,20 +80,12 @@ ComboBox {
text : root.displayText
color: !root.enabled ? Style.main.textDisabled : ( root.down ? Style.main.background : Style.main.text )
}
}
displayText: {
if (view.currentIndex >= 0) {
if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels")
var tgtName = view.currentItem.folderName
var tgtIcon = view.currentItem.folderIcon
var tgtColor = view.currentItem.folderColor
if (tgtIcon != Style.fa.folder_open) {
return tgtIcon + " " + tgtName
}
return '<font color="'+tgtColor+'">'+ tgtIcon + "</font> " + tgtName
if (!root.isFolderType) return qsTr("Add/Remove labels")
return view.currentItem.folderName
}
if (root.isFolderType) return qsTr("No folder selected")
return qsTr("No labels selected")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -77,6 +77,8 @@ func (f *FrontendQt) StartImport(email string) { // TODO email not needed
log.Trace("Starting import")
f.Qml.SetProgressDescription("init") // TODO use const
f.Qml.SetProgressImported(0)
f.Qml.SetProgressSkipped(0)
f.Qml.SetProgressFails(0)
f.Qml.SetProgress(0.0)
f.Qml.SetTotal(1)

View File

@ -43,6 +43,8 @@ type GoQMLInterface struct {
_ string `property:lastError`
_ float32 `property:progress`
_ string `property:progressDescription`
_ int `property:progressImported`
_ int `property:progressSkipped`
_ int `property:progressFails`
_ int `property:total`
_ string `property:importLogFileName`

View File

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

View File

@ -40,17 +40,17 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/kardianos/osext"
"github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
"github.com/therecipe/qt/core"
"github.com/therecipe/qt/gui"
@ -187,7 +187,6 @@ func (s *FrontendQt) watchEvents() {
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent)
newUserCh := s.getEventChannel(events.UserRefreshEvent)
certIssue := s.getEventChannel(events.TLSCertIssue)
imapCertIssue := s.getEventChannel(events.IMAPTLSBadCert)
for {
select {
case errorDetails := <-errorCh:
@ -227,8 +226,6 @@ func (s *FrontendQt) watchEvents() {
s.Qml.LoadAccounts()
case <-certIssue:
s.Qml.ShowCertIssue()
case <-imapCertIssue:
s.Qml.ShowIMAPCertTroubleshoot()
}
}
}

View File

@ -135,7 +135,6 @@ type GoQMLInterface struct {
_ func(x, y float32) `slot:"saveOutgoingNoEncPopupCoord"`
_ func(recipient string) `signal:"showNoActiveKeyForRecipient"`
_ func() `signal:"showCertIssue"`
_ func() `signal:"ShowIMAPCertTroubleshoot"`
_ func() `slot:"startUpdate"`
_ func(hasError bool) `signal:"updateFinished"`

View File

@ -23,13 +23,11 @@ import (
"sync"
"time"
imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-imap"
goIMAPBackend "github.com/emersion/go-imap/backend"
"github.com/sirupsen/logrus"
)
type panicHandler interface {
@ -45,9 +43,6 @@ type imapBackend struct {
users map[string]*imapUser
usersLocker sync.Locker
lastMailClient imapid.ID
lastMailClientLocker sync.Locker
imapCache map[string]map[string]string
imapCachePath string
imapCacheLock *sync.RWMutex
@ -87,9 +82,6 @@ func newIMAPBackend(
users: map[string]*imapUser{},
usersLocker: &sync.Mutex{},
lastMailClient: imapid.ID{imapid.FieldName: clientNone},
lastMailClientLocker: &sync.Mutex{},
imapCachePath: cfg.GetIMAPCachePath(),
imapCacheLock: &sync.RWMutex{},
}
@ -164,7 +156,9 @@ func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMA
if err := imapUser.user.CheckBridgeLogin(password); err != nil {
log.WithError(err).Error("Could not check bridge password")
_ = imapUser.Logout()
if err := imapUser.Logout(); err != nil {
log.WithError(err).Warn("Could not logout user after unsuccessful login check")
}
// Apple Mail sometimes generates a lot of requests very quickly.
// It's therefore good to have a timeout after a bad login so that we can slow
// those requests down a little bit.
@ -192,23 +186,6 @@ func (ib *imapBackend) CreateMessageLimit() *uint32 {
return nil
}
func (ib *imapBackend) setLastMailClient(id imapid.ID) {
ib.lastMailClientLocker.Lock()
defer ib.lastMailClientLocker.Unlock()
if name, ok := id[imapid.FieldName]; ok && ib.lastMailClient[imapid.FieldName] != name {
ib.lastMailClient = imapid.ID{}
for k, v := range id {
ib.lastMailClient[k] = v
}
log.Warn("Mail Client ID changed to ", ib.lastMailClient)
ib.bridge.SetCurrentClient(
ib.lastMailClient[imapid.FieldName],
ib.lastMailClient[imapid.FieldVersion],
)
}
}
// monitorDisconnectedUsers removes users when it receives a close connection event for them.
func (ib *imapBackend) monitorDisconnectedUsers() {
ch := make(chan string)
@ -220,11 +197,3 @@ func (ib *imapBackend) monitorDisconnectedUsers() {
ib.deleteUser(address)
}
}
func (ib *imapBackend) upgradeError(err error) {
logrus.WithError(err).Error("IMAP connection couldn't be upgraded to TLS during STARTTLS")
if strings.Contains(err.Error(), "remote error: tls: bad certificate") {
ib.eventListener.Emit(events.IMAPTLSBadCert, err.Error())
}
}

View File

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

View File

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

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 (
fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP).
fetchAttachmentsWorkers = 5 // In how many workers to fetch attachments (for one message).
clientAppleMail = "Mac OS X Mail" //nolint[deadcode]
clientThunderbird = "Thunderbird" //nolint[deadcode]
clientOutlookMac = "Microsoft Outlook for Mac" //nolint[deadcode]
clientOutlookWin = "Microsoft Outlook" //nolint[deadcode]
clientNone = ""
)
var (

View File

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

View File

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

View File

@ -57,7 +57,11 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
return im.addOrRemoveFlags(operation, messageIDs, flags)
}
func (im *imapMailbox) setFlags(messageIDs, flags []string) error {
// setFlags is used for FLAGS command (not +FLAGS or -FLAGS), which means
// to set flags passed as an argument and unset the rest. For example,
// if message is not read, is flagged and is not deleted, call FLAGS \Seen
// should flag message as read, unflagged and keep undeleted.
func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
seen := false
flagged := false
deleted := false
@ -77,29 +81,48 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error {
}
if seen {
_ = im.storeMailbox.MarkMessagesRead(messageIDs)
if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
return err
}
} else {
_ = im.storeMailbox.MarkMessagesUnread(messageIDs)
if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil {
return err
}
}
if flagged {
_ = im.storeMailbox.MarkMessagesStarred(messageIDs)
if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
return err
}
} else {
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs)
if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil {
return err
}
}
if deleted {
_ = im.storeMailbox.DeleteMessages(messageIDs)
if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil {
return err
}
} else {
if err := im.storeMailbox.MarkMessagesUndeleted(messageIDs); err != nil {
return err
}
}
// Spam should not be taken into action here as Outlook is using FLAGS
// without preserving junk flag. Probably it's because junk is not standard
// in the rfc3501 and thus Outlook expects calling FLAGS \Seen will not
// change the state of junk or other non-standard flags.
// Still, its safe to label as spam once any client sends the request.
if spam {
spamMailbox, err := im.storeAddress.GetMailbox("Spam")
if err != nil {
return err
}
if spam {
_ = spamMailbox.LabelMessages(messageIDs)
} else {
_ = spamMailbox.UnlabelMessages(messageIDs)
if err := spamMailbox.LabelMessages(messageIDs); err != nil {
return err
}
}
return nil
@ -111,22 +134,36 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
case imap.SeenFlag:
switch operation {
case imap.AddFlags:
_ = im.storeMailbox.MarkMessagesRead(messageIDs)
if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
return err
}
case imap.RemoveFlags:
_ = im.storeMailbox.MarkMessagesUnread(messageIDs)
if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil {
return err
}
}
case imap.FlaggedFlag:
switch operation {
case imap.AddFlags:
_ = im.storeMailbox.MarkMessagesStarred(messageIDs)
if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
return err
}
case imap.RemoveFlags:
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs)
if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil {
return err
}
}
case imap.DeletedFlag:
if operation == imap.RemoveFlags {
break // Nothing to do, no message has the \Deleted flag.
switch operation {
case imap.AddFlags:
if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil {
return err
}
case imap.RemoveFlags:
if err := im.storeMailbox.MarkMessagesUndeleted(messageIDs); err != nil {
return err
}
}
_ = im.storeMailbox.DeleteMessages(messageIDs)
case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag:
// Not supported.
case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag:
@ -140,9 +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
// will automatically take care of label removal.
case imap.AddFlags:
_ = storeMailbox.LabelMessages(messageIDs)
if err := storeMailbox.LabelMessages(messageIDs); err != nil {
return err
}
case imap.RemoveFlags:
_ = storeMailbox.UnlabelMessages(messageIDs)
if err := storeMailbox.UnlabelMessages(messageIDs); err != nil {
return err
}
}
}
}
@ -186,6 +227,20 @@ func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel
return err
}
deletedIDs := []string{}
allDeletedIDs, err := im.storeMailbox.GetDeletedAPIIDs()
if err != nil {
log.WithError(err).Warn("Problem to get deleted API IDs")
} else {
for _, messageID := range messageIDs {
for _, deletedID := range allDeletedIDs {
if messageID == deletedID {
deletedIDs = append(deletedIDs, deletedID)
}
}
}
}
// Label messages first to not lose them. If message is only in trash and we unlabel
// it, it will be removed completely and we cannot label it back.
if err := targetStoreMailbox.LabelMessages(messageIDs); err != nil {
@ -197,6 +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)
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) {
messageFlagsMap[imap.RecentFlag] = true
}
if storeMessage.IsMarkedDeleted() {
messageFlagsMap[imap.DeletedFlag] = true
}
flagMatch := true
for _, flag := range criteria.WithFlags {
@ -383,6 +448,12 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
im.panicHandler.HandlePanic()
}()
// EXPUNGE cannot be sent during listing and can come only from
// the event loop, so we prevent any server side update to avoid
// the problem.
im.storeUser.PauseEventLoop(true)
defer im.storeUser.PauseEventLoop(false)
var markAsReadIDs []string
markAsReadMutex := &sync.Mutex{}

View File

@ -28,6 +28,7 @@ import (
imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/imap/id"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-imap"
@ -57,37 +58,14 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
s.AllowInsecureAuth = true
s.ErrorLog = newServerErrorLogger("server-imap")
s.AutoLogout = 30 * time.Minute
s.UpgradeError = imapBackend.upgradeError
serverID := imapid.ID{
imapid.FieldName: "ProtonMail",
imapid.FieldName: "ProtonMail Bridge",
imapid.FieldVendor: "Proton Technologies AG",
imapid.FieldSupportURL: "https://protonmail.com/support",
}
s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
conn.Server().ForEachConn(func(candidate imapserver.Conn) {
if id, ok := candidate.(imapid.Conn); ok {
if conn.Context() == candidate.Context() {
// ID is not available right at the beginning of the connection.
// Clients send ID quickly after AUTH. We need to wait for it.
go func() {
start := time.Now()
for {
if id.ID() != nil {
imapBackend.setLastMailClient(id.ID())
break
}
if time.Since(start) > 10*time.Second {
break
}
time.Sleep(100 * time.Millisecond)
}
}()
}
}
})
return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil {
@ -105,7 +83,7 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
imapidle.NewExtension(),
imapmove.NewExtension(),
imapspecialuse.NewExtension(),
imapid.NewExtension(serverID),
id.NewExtension(serverID, imapBackend.bridge),
imapquota.NewExtension(),
imapappendlimit.NewExtension(),
imapunselect.NewExtension(),

View File

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

View File

@ -25,6 +25,7 @@
package uidplus
import (
"errors"
"fmt"
"github.com/emersion/go-imap"
@ -113,18 +114,43 @@ func (os *OrderedSeq) String() string {
return out
}
// UIDExpunge implements server.Handler but has no effect because Bridge is not
// using EXPUNGE at all. The message is deleted right after it was flagged as
// \Deleted Bridge should simply ignore this command with empty `OK` response.
//
// If not implemented it would cause harmless IMAP error.
//
// This overrides the standard EXPUNGE functionality.
type UIDExpunge struct{}
// UIDExpunge implements server.Handler but Bridge is not supporting
// UID EXPUNGE with specific UIDs.
type UIDExpunge struct {
expunge *server.Expunge
}
func (e *UIDExpunge) Parse(fields []interface{}) error { log.Traceln("parse", fields); return nil }
func (e *UIDExpunge) Handle(conn server.Conn) error { log.Traceln("handle"); return nil }
func (e *UIDExpunge) UidHandle(conn server.Conn) error { log.Traceln("uid handle"); return nil } //nolint[golint]
func newUIDExpunge() *UIDExpunge {
return &UIDExpunge{expunge: &server.Expunge{}}
}
func (e *UIDExpunge) Parse(fields []interface{}) error {
if len(fields) < 1 {
return e.expunge.Parse(fields)
}
// RFC4315#section-2.1
// The UID EXPUNGE command permanently removes all messages that both
// have the \Deleted flag set and have a UID that is included in the
// specified sequence set from the currently selected mailbox. If a
// message either does not have the \Deleted flag set or has a UID
// that is not included in the specified sequence set, it is not
// affected.
//
// Current implementation supports only deletion of all messages
// marked as deleted. It will probably need mailbox interface change:
// ExpungeUIDs(seqSet). Not sure how to combine with original
// e.expunge.Handle().
return errors.New("UID EXPUNGE with UIDs is not supported")
}
func (e *UIDExpunge) Handle(conn server.Conn) error {
return e.expunge.Handle(conn)
}
func (e *UIDExpunge) UidHandle(conn server.Conn) error { //nolint[golint]
return e.expunge.Handle(conn)
}
type extension struct{}
@ -143,7 +169,7 @@ func (ext *extension) Capabilities(c server.Conn) []string {
func (ext *extension) Command(name string) server.HandlerFactory {
if name == "EXPUNGE" {
return func() server.Handler {
return &UIDExpunge{}
return newUIDExpunge()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ type ContactMetadata struct {
Keys []string
Scheme string
Sign bool
SignMissing bool
SignIsSet bool
Encrypt bool
MIMEType string
}
@ -72,12 +72,12 @@ func GetContactMetadataFromVCards(cards []pmapi.Card, email string) (contactMeta
// Warn: ParseBool treats 1, T, True, true as true and 0, F, Fale, false as false.
// However PMEL declares 'true' is true, 'false' is false. every other string is true
encrypt, _ := strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMEncrypt, group))
var sign, signMissing bool
var sign, signIsSet bool
if len(parsedCard[FieldPMSign]) == 0 {
signMissing = true
signIsSet = false
} else {
sign, _ = strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMSign, group))
signMissing = false
signIsSet = true
}
mimeType := parsedCard.GetValueByGroup(FieldPMMIMEType, group)
return &ContactMetadata{
@ -85,7 +85,7 @@ func GetContactMetadataFromVCards(cards []pmapi.Card, email string) (contactMeta
Keys: keys,
Scheme: scheme,
Sign: sign,
SignMissing: signMissing,
SignIsSet: signIsSet,
Encrypt: encrypt,
MIMEType: mimeType,
}, nil

View File

@ -69,7 +69,7 @@ func (storeAddress *Address) init(foldersAndLabels []*pmapi.Label) (err error) {
prefix := getLabelPrefix(label)
var mailbox *Mailbox
if mailbox, err = txNewMailbox(tx, storeAddress, label.ID, prefix, label.Name, label.Color); err != nil {
if mailbox, err = txNewMailbox(tx, storeAddress, label.ID, prefix, label.Path, label.Color); err != nil {
storeAddress.log.
WithError(err).
WithField("labelID", label.ID).

View File

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

View File

@ -45,7 +45,9 @@ func (c *Cache) getEventID(userID string) string {
c.lock.Lock()
defer c.lock.Unlock()
_ = c.loadCache()
if err := c.loadCache(); err != nil {
log.WithError(err).Warn("Problem to load store cache")
}
if c.cache == nil {
c.cache = map[string]map[string]string{}

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.Update = imapBackend.NewUpdate(address, "")
update.StatusResp = &imap.StatusResp{
@ -46,25 +46,35 @@ func (store *Store) imapNotice(address, notice string) {
Info: notice,
}
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{
"address": address,
"mailbox": mailboxName,
"seqNum": sequenceNumber,
"uid": uid,
"flags": message.GetFlags(msg),
"deleted": hasDeletedFlag,
}).Trace("IDLE update")
update := new(imapBackend.MessageUpdate)
update.Update = imapBackend.NewUpdate(address, mailboxName)
update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
update.Message.Flags = message.GetFlags(msg)
if hasDeletedFlag {
update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag)
}
update.Message.Uid = uid
store.imapSendUpdate(update)
return update
}
func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumber uint32) {
func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumber uint32) *imapBackend.ExpungeUpdate {
store.log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
@ -74,9 +84,10 @@ func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumbe
update.Update = imapBackend.NewUpdate(address, mailboxName)
update.SeqNum = sequenceNumber
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{
"address": address,
"mailbox": mailboxName,
@ -89,9 +100,10 @@ func (store *Store) imapMailboxCreated(address, mailboxName string) {
Name: mailboxName,
}
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{
"address": address,
"mailbox": mailboxName,
@ -106,6 +118,7 @@ func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread
update.MailboxStatus.Unseen = uint32(unread)
update.MailboxStatus.UnseenSeqNum = uint32(unreadSeqNum)
store.imapSendUpdate(update)
return update
}
func (store *Store) imapSendUpdate(update imapBackend.Update) {
@ -116,7 +129,7 @@ func (store *Store) imapSendUpdate(update imapBackend.Update) {
select {
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
case store.imapUpdates <- update:
}

View File

@ -38,7 +38,8 @@ type eventLoop struct {
pollCh chan chan struct{}
stopCh chan struct{}
notifyStopCh chan struct{}
isRunning bool
isRunning bool // The whole event loop is running.
isTickerPaused bool // The periodic loop is paused (but the event loop itself is still running).
hasInternet bool
pollCounter int
@ -59,6 +60,7 @@ func newEventLoop(cache *Cache, store *Store, user BridgeUser, events listener.L
currentEventID: cache.getEventID(user.ID()),
pollCh: make(chan chan struct{}),
isRunning: false,
isTickerPaused: false,
log: eventLog,
@ -68,10 +70,6 @@ func newEventLoop(cache *Cache, store *Store, user BridgeUser, events listener.L
}
}
func (loop *eventLoop) IsRunning() bool {
return loop.isRunning
}
func (loop *eventLoop) client() pmapi.Client {
return loop.store.client()
}
@ -156,6 +154,10 @@ func (loop *eventLoop) loop() {
close(loop.notifyStopCh)
return
case <-t.C:
if loop.isTickerPaused {
loop.log.Trace("Event loop paused, skipping")
continue
}
// Randomise periodic calls within range pollInterval ± pollSpread to reduces potential load spikes on API.
time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond)
case eventProcessedCh = <-loop.pollCh:

View File

@ -24,6 +24,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"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.
go m.store.eventLoop.start()
require.Eventually(t, func() bool {
msg, err := m.store.getMessageFromDB("msg1")
var err error
assert.Eventually(t, func() bool {
var msg *pmapi.Message
msg, err = m.store.getMessageFromDB("msg1")
return err == nil && msg.Subject == newSubject
}, time.Second, 10*time.Millisecond)
require.NoError(t, err)
}
func TestEventLoopUpdateMessage(t *testing.T) {

View File

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

View File

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

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) {
b := storeMailbox.txGetAPIIDsBucket(tx)
return storeMailbox.txGetUIDFromBucket(storeMailbox.txGetAPIIDsBucket(tx), apiID)
}
// txGetUIDFromBucket expects pointer to API bucket.
func (storeMailbox *Mailbox) txGetUIDFromBucket(b *bolt.Bucket, apiID string) (uint32, error) {
v := b.Get([]byte(apiID))
if v == nil {
return 0, ErrNoSuchAPIID
@ -137,6 +141,19 @@ func (storeMailbox *Mailbox) txGetUID(tx *bolt.Tx, apiID string) (uint32, error)
return btoi(v), nil
}
// GetDeletedAPIIDs returns API IDs in this mailbox for message ID.
func (storeMailbox *Mailbox) GetDeletedAPIIDs() (apiIDs []string, err error) {
err = storeMailbox.db().Update(func(tx *bolt.Tx) error {
b := storeMailbox.txGetDeletedIDsBucket(tx)
c := b.Cursor()
for k, _ := c.First(); k != nil; k, _ = c.Next() {
apiIDs = append(apiIDs, string(k))
}
return nil
})
return
}
// getSequenceNumber returns IMAP sequence number in the mailbox for the message with the given API ID `apiID`.
func (storeMailbox *Mailbox) getSequenceNumber(apiID string) (seqNum uint32, err error) {
err = storeMailbox.db().View(func(tx *bolt.Tx) error {

View File

@ -18,12 +18,16 @@
package store
import (
"time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)
// ErrAllMailOpNotAllowed is error user when user tries to do unsupported
// operation on All Mail folder
var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder")
// GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage`
@ -96,11 +100,8 @@ func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error {
// It has to be propagated to all the same messages in all mailboxes.
// The propagation is processed by the event loop.
func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error {
log.WithFields(logrus.Fields{
"messages": apiIDs,
"label": storeMailbox.labelID,
"mailbox": storeMailbox.Name,
}).Trace("Unlabeling messages")
storeMailbox.log.WithField("messages", apiIDs).
Trace("Unlabeling messages")
if storeMailbox.labelID == pmapi.AllMailLabel {
return ErrAllMailOpNotAllowed
}
@ -129,6 +130,9 @@ func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error {
ids = append(ids, apiID)
}
}
if len(ids) == 0 {
return nil
}
return storeMailbox.client().MarkMessagesRead(ids)
}
@ -170,22 +174,83 @@ func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error {
return storeMailbox.client().UnlabelMessages(apiIDs, pmapi.StarredLabel)
}
// DeleteMessages deletes messages.
// If the mailbox is All Mail or All Sent, it does nothing.
// If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted.
// In all other cases the message is only removed from the mailbox.
func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
// MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API
// until RemoveDeleted is called
func (storeMailbox *Mailbox) MarkMessagesDeleted(apiIDs []string) error {
log.WithFields(logrus.Fields{
"messages": apiIDs,
"label": storeMailbox.labelID,
"mailbox": storeMailbox.Name,
}).Trace("Deleting messages")
}).Trace("Marking messages as deleted")
if storeMailbox.labelID == pmapi.AllMailLabel {
return ErrAllMailOpNotAllowed
}
return storeMailbox.store.db.Update(func(tx *bolt.Tx) error {
return storeMailbox.txMarkMessagesAsDeleted(tx, apiIDs, true)
})
}
// MarkMessagesUndeleted removes local flag \Deleted. This is not propagated to
// API.
func (storeMailbox *Mailbox) MarkMessagesUndeleted(apiIDs []string) error {
log.WithFields(logrus.Fields{
"messages": apiIDs,
"label": storeMailbox.labelID,
"mailbox": storeMailbox.Name,
}).Trace("Marking messages as undeleted")
if storeMailbox.labelID == pmapi.AllMailLabel {
return ErrAllMailOpNotAllowed
}
return storeMailbox.store.db.Update(func(tx *bolt.Tx) error {
return storeMailbox.txMarkMessagesAsDeleted(tx, apiIDs, false)
})
}
// RemoveDeleted sends request to API to remove message from mailbox.
// If the mailbox is All Mail or All Sent, it does nothing.
// If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted.
// In all other cases the message is only removed from the mailbox.
func (storeMailbox *Mailbox) RemoveDeleted() error {
storeMailbox.log.Trace("Deleting messages")
apiIDs, err := storeMailbox.GetDeletedAPIIDs()
if err != nil {
return err
}
if len(apiIDs) == 0 {
storeMailbox.log.Debug("List to expunge is empty")
return nil
}
defer storeMailbox.pollNow()
switch storeMailbox.labelID {
case pmapi.AllMailLabel, pmapi.AllSentLabel:
break
case pmapi.TrashLabel, pmapi.SpamLabel:
if err := storeMailbox.deleteFromTrashOrSpam(apiIDs); err != nil {
return err
}
case pmapi.DraftLabel:
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
return err
}
default:
if err := storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID); err != nil {
return err
}
}
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 {
@ -211,7 +276,7 @@ func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
}
if len(messageIDsToUnlabel) > 0 {
if err := storeMailbox.client().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil {
log.WithError(err).Warning("Cannot unlabel before deleting")
l.WithError(err).Warning("Cannot unlabel before deleting")
}
}
if len(messageIDsToDelete) > 0 {
@ -219,15 +284,7 @@ func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
return err
}
}
case pmapi.DraftLabel:
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
return err
}
default:
if err := storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID); err != nil {
return err
}
}
return nil
}
@ -270,7 +327,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
// Buckets are not initialized right away because it's a heavy operation.
// The best option is to get the same bucket only once and only when needed.
var apiBucket, imapBucket *bolt.Bucket
var apiBucket, imapBucket, deletedBucket *bolt.Bucket
for _, msg := range msgs {
if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) {
continue
@ -289,12 +346,15 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
}
} else {
uidb := apiBucket.Get([]byte(msg.ID))
if uidb != nil {
if imapBucket == nil {
imapBucket = storeMailbox.txGetIMAPIDsBucket(tx)
}
seqNum, seqErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb)
if deletedBucket == nil {
deletedBucket = storeMailbox.txGetDeletedIDsBucket(tx)
}
isMarkedAsDeleted := deletedBucket.Get([]byte(msg.ID)) != nil
if seqErr == nil {
storeMailbox.store.imapUpdateMessage(
storeMailbox.storeAddress.address,
@ -302,6 +362,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
btoi(uidb),
seqNum,
msg,
isMarkedAsDeleted,
)
}
continue
@ -335,6 +396,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
uid,
seqNum,
msg,
false, // new message is never marked as deleted
)
shouldSendMailboxUpdate = true
}
@ -359,6 +421,7 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
}
imapBucket := storeMailbox.txGetIMAPIDsBucket(tx)
deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx)
seqNum, seqNumErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb)
if seqNumErr != nil {
@ -373,6 +436,10 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
return errors.Wrap(err, "cannot delete from API bucket")
}
if err := deletedBucket.Delete(apiIDb); err != nil {
return errors.Wrap(err, "cannot delete from mark-as-deleted bucket")
}
if seqNumErr == nil {
storeMailbox.store.imapDeleteMessage(
storeMailbox.storeAddress.address,
@ -401,3 +468,58 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
)
return nil
}
func (storeMailbox *Mailbox) txMarkMessagesAsDeleted(tx *bolt.Tx, apiIDs []string, markAsDeleted bool) error {
// Load all buckets before looping over apiIDs
metaBucket := tx.Bucket(metadataBucket)
apiBucket := storeMailbox.txGetAPIIDsBucket(tx)
uidBucket := storeMailbox.txGetIMAPIDsBucket(tx)
deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx)
for _, apiID := range apiIDs {
if markAsDeleted {
if err := deletedBucket.Put([]byte(apiID), []byte{1}); err != nil {
return err
}
} else {
if err := deletedBucket.Delete([]byte(apiID)); err != nil {
return err
}
}
msg, err := storeMailbox.store.txGetMessageFromBucket(metaBucket, apiID)
if err != nil {
return err
}
uid, err := storeMailbox.txGetUIDFromBucket(apiBucket, apiID)
if err != nil {
return err
}
seqNum, err := storeMailbox.txGetSequenceNumberOfUID(uidBucket, itob(uid))
if err != nil {
return err
}
// In order to send flags in format
// S: * 2 FETCH (FLAGS (\Deleted \Seen))
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
}
// IsMarkedDeleted returns true if message is marked as deleted for specific
// mailbox
func (message *Message) IsMarkedDeleted() bool {
isMarkedAsDeleted := false
err := message.storeMailbox.db().View(func(tx *bolt.Tx) error {
isMarkedAsDeleted = message.storeMailbox.txGetDeletedIDsBucket(tx).Get([]byte(message.msg.ID)) != nil
return nil
})
if err != nil {
message.storeMailbox.log.WithError(err).Error("Not able to retrieve deleted mark, assuming false.")
return false
}
return isMarkedAsDeleted
}
// SetSize updates the information about size of decrypted message which can be
// used for IMAP. This should not trigger any IMAP update.
// NOTE: The size from the server corresponds to pure body bytes. Hence it

View File

@ -5,9 +5,10 @@
package mocks
import (
reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockPanicHandler is a mock of PanicHandler interface
@ -105,6 +106,18 @@ func (m *MockBridgeUser) EXPECT() *MockBridgeUserMockRecorder {
return m.recorder
}
// CloseAllConnections mocks base method
func (m *MockBridgeUser) CloseAllConnections() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "CloseAllConnections")
}
// CloseAllConnections indicates an expected call of CloseAllConnections
func (mr *MockBridgeUserMockRecorder) CloseAllConnections() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseAllConnections", reflect.TypeOf((*MockBridgeUser)(nil).CloseAllConnections))
}
// CloseConnection mocks base method
func (m *MockBridgeUser) CloseConnection(arg0 string) {
m.ctrl.T.Helper()

View File

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

View File

@ -70,6 +70,8 @@ var (
// * {imapUID} -> string messageID
// * api_ids
// * {messageID} -> uint32 imapUID
// * deleted_ids (can be missing or have no keys)
// * {messageID} -> true
metadataBucket = []byte("metadata") //nolint[gochecknoglobals]
countsBucket = []byte("counts") //nolint[gochecknoglobals]
addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals]
@ -78,6 +80,7 @@ var (
mailboxesBucket = []byte("mailboxes") //nolint[gochecknoglobals]
imapIDsBucket = []byte("imap_ids") //nolint[gochecknoglobals]
apiIDsBucket = []byte("api_ids") //nolint[gochecknoglobals]
deletedIDsBucket = []byte("deleted_ids") //nolint[gochecknoglobals]
mboxVersionBucket = []byte("mailboxes_version") //nolint[gochecknoglobals]
// ErrNoSuchAPIID when mailbox does not have API ID.
@ -348,6 +351,18 @@ func (store *Store) addAddress(address, addressID string, labels []*pmapi.Label)
return
}
// PauseEventLoop sets whether the ticker is periodically polling or not.
func (store *Store) PauseEventLoop(pause bool) {
store.lock.Lock()
defer store.lock.Unlock()
store.log.WithField("pause", pause).Info("Pausing event loop")
if store.eventLoop != nil {
store.eventLoop.isTickerPaused = pause
}
}
// Close stops the event loop and closes the database to free the file.
func (store *Store) Close() error {
store.lock.Lock()

View File

@ -26,6 +26,10 @@ import (
bolt "go.etcd.io/bbolt"
)
func (loop *eventLoop) IsRunning() bool {
return loop.isRunning
}
// TestSync triggers a sync of the store.
func (store *Store) TestSync() {
store.lock.Lock()
@ -102,11 +106,13 @@ func txDumpMailsFactory(tb assert.TestingT) func(tx *bolt.Tx) error {
err := mailboxes.ForEach(func(mboxName, mboxData []byte) error {
fmt.Println("mbox:", string(mboxName))
b := mailboxes.Bucket(mboxName).Bucket(imapIDsBucket)
deletedMailboxes := mailboxes.Bucket(mboxName).Bucket(deletedIDsBucket)
c := b.Cursor()
i := 0
for imapID, apiID := c.First(); imapID != nil; imapID, apiID = c.Next() {
i++
fmt.Println(" ", i, "imap", btoi(imapID), "api", string(apiID))
isDeleted := deletedMailboxes != nil && deletedMailboxes.Get(apiID) != nil
fmt.Println(" ", i, "imap", btoi(imapID), "api", string(apiID), "isDeleted", isDeleted)
data := metadata.Get(apiID)
if !assert.NotNil(tb, data) {
continue

View File

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

View File

@ -210,7 +210,9 @@ func (store *Store) deleteMailboxEvent(labelID string) error {
store.lock.Lock()
defer store.lock.Unlock()
_ = store.removeMailboxCount(labelID)
if err := store.removeMailboxCount(labelID); err != nil {
log.WithError(err).Warn("Problem to remove mailbox counts while deleting mailbox")
}
for _, a := range store.addresses {
if err := a.deleteMailboxEvent(labelID); err != nil {

View File

@ -63,7 +63,7 @@ func (store *Store) CreateDraft(
attachmentReaders = append(attachmentReaders, strings.NewReader(attachedPublicKey))
publicKeyAttachment := &pmapi.Attachment{
Name: attachedPublicKeyName + ".asc",
MIMEType: "application/pgp-key",
MIMEType: "application/pgp-keys",
Header: textproto.MIMEHeader{},
}
attachments = append(attachments, publicKeyAttachment)
@ -143,8 +143,10 @@ func (store *Store) getMessageFromDB(apiID string) (msg *pmapi.Message, err erro
}
func (store *Store) txGetMessage(tx *bolt.Tx, apiID string) (*pmapi.Message, error) {
b := tx.Bucket(metadataBucket)
return store.txGetMessageFromBucket(tx.Bucket(metadataBucket), apiID)
}
func (store *Store) txGetMessageFromBucket(b *bolt.Bucket, apiID string) (*pmapi.Message, error) {
msgb := b.Get([]byte(apiID))
if msgb == nil {
return nil, ErrNoSuchAPIID

View File

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

View File

@ -5,9 +5,10 @@
package mocks
import (
reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockPanicHandler is a mock of PanicHandler interface

View File

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

View File

@ -0,0 +1,35 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package transfer
// ProgressCounts holds counts counted by Progress.
type ProgressCounts struct {
Failed,
Skipped,
Imported,
Exported,
Added,
Total uint
}
// Progress returns ratio between processed messages (fully imported, skipped
// and failed ones) and total number of messages as percentage (0 - 1).
func (c *ProgressCounts) Progress() float32 {
progressed := c.Imported + c.Skipped + c.Failed
return float32(progressed) / float32(c.Total)
}

View File

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

View File

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

View File

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

View File

@ -31,6 +31,8 @@ type IMAPProvider struct {
addr string
client *imapClient.Client
timeIt *timeIt
}
// NewIMAPProvider returns new IMAPProvider.
@ -39,6 +41,8 @@ func NewIMAPProvider(username, password, host, port string) (*IMAPProvider, erro
username: username,
password: password,
addr: net.JoinHostPort(host, port),
timeIt: newTimeIt("imap"),
}
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")
defer log.Info("Finished transfer from IMAP to channel")
p.timeIt.clear()
defer p.timeIt.logResults()
imapMessageInfoMap := p.loadMessageInfoMap(rules, progress)
for rule := range rules.iterateActiveRules() {
@ -78,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 {
p.timeIt.start("load", rule.SourceMailbox.Name)
defer p.timeIt.stop("load", rule.SourceMailbox.Name)
messagesInfo := map[string]imapMessageInfo{}
pageStart := uint32(1)
@ -118,7 +124,7 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
uid: imapMessage.Uid,
size: imapMessage.Size,
}
progress.addMessage(id, rule)
progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
}
progress.callWrap(func() error {
@ -199,13 +205,18 @@ func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<-
progress.messageExported(id, body, err)
if err == nil {
msg := p.exportMessage(rule, id, imapMessage, body)
p.timeIt.stop("fetch", rule.SourceMailbox.Name)
ch <- msg
p.timeIt.start("fetch", rule.SourceMailbox.Name)
}
}
p.timeIt.start("fetch", rule.SourceMailbox.Name)
progress.callWrap(func() error {
return p.uidFetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
})
p.timeIt.stop("fetch", rule.SourceMailbox.Name)
}
func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Message, body []byte) Message {
@ -220,7 +231,7 @@ func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Me
ID: id,
Unread: unread,
Body: body,
Source: rule.SourceMailbox,
Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes,
}
}

View File

@ -18,7 +18,9 @@
package transfer
import (
"crypto/tls"
"net"
"strings"
"time"
imapID "github.com/ProtonMail/go-imap-id"
@ -146,7 +148,19 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
if host == "127.0.0.1" {
client, err = imapClient.Dial(p.addr)
} 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 {
return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}}

View File

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

View File

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

View File

@ -33,8 +33,10 @@ type PMAPIProvider struct {
addressID string
keyRing *crypto.KeyRing
importMsgReqMap map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
importMsgReqSize int
nextImportRequests map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
nextImportRequestsSize int
timeIt *timeIt
}
// NewPMAPIProvider returns new PMAPIProvider.
@ -45,8 +47,10 @@ func NewPMAPIProvider(config *pmapi.ClientConfig, clientManager ClientManager, u
userID: userID,
addressID: addressID,
importMsgReqMap: map[string]*pmapi.ImportMsgReq{},
importMsgReqSize: 0,
nextImportRequests: map[string]*pmapi.ImportMsgReq{},
nextImportRequestsSize: 0,
timeIt: newTimeIt("pmapi"),
}
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")
defer log.Info("Finished transfer from PMAPI to channel")
p.timeIt.clear()
defer p.timeIt.logResults()
// TransferTo cannot end sooner than loadCounts goroutine because
// loadCounts writes to channel in progress which would be closed.
// 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)
progress.addMessage(msgID, rule)
progress.addMessage(msgID, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
msg, err := p.exportMessage(rule, progress, pmapiMessage.ID, msgID, skipEncryptedMessages)
progress.messageExported(msgID, msg.Body, err)
if err == nil {
@ -147,6 +150,9 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
return err
})
p.timeIt.start("build", msgID)
defer p.timeIt.stop("build", msgID)
msgBuilder := pkgMessage.NewBuilder(p.client(), msg)
msgBuilder.EncryptedToHTML = false
_, body, err := msgBuilder.BuildMessage()
@ -171,7 +177,7 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
ID: msgID,
Unread: unread,
Body: body,
Source: rule.SourceMailbox,
Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes,
}, nil
}

View File

@ -22,6 +22,7 @@ import (
"fmt"
"io"
"io/ioutil"
"sync"
pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -32,6 +33,7 @@ import (
const (
pmapiImportBatchMaxItems = 10
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.
@ -72,10 +74,16 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
log.Info("Started 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
// old stuff from previous cancelled run.
p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{}
p.importMsgReqSize = 0
p.nextImportRequests = map[string]*pmapi.ImportMsgReq{}
p.nextImportRequestsSize = 0
preparedImportRequestsCh := make(chan map[string]*pmapi.ImportMsgReq)
wg := p.startImportWorkers(progress, preparedImportRequestsCh)
for msg := range ch {
if progress.shouldStop() {
@ -85,13 +93,15 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
if p.isMessageDraft(msg) {
p.transferDraft(rules, progress, msg)
} else {
p.transferMessage(rules, progress, msg)
p.transferMessage(rules, progress, msg, preparedImportRequestsCh)
}
}
if len(p.importMsgReqMap) > 0 {
p.importMessages(progress)
if len(p.nextImportRequests) > 0 {
preparedImportRequestsCh <- p.nextImportRequests
}
close(preparedImportRequestsCh)
wg.Wait()
}
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")
}
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")
}
@ -125,7 +138,7 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
attachments := message.Attachments
message.Attachments = nil
draft, err := p.createDraft(message, "", pmapi.DraftActionReply)
draft, err := p.createDraft(msg.ID, message, "", pmapi.DraftActionReply)
if err != nil {
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")
}
p.timeIt.start("encrypt", msg.ID)
r = bytes.NewReader(attachmentBody)
encReader, err := attachment.Encrypt(p.keyRing, r)
p.timeIt.stop("encrypt", msg.ID)
if err != nil {
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 {
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
}
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)
if err != nil {
progress.messageImported(msg.ID, "", err)
return
}
importMsgReqSize := len(importMsgReq.Body)
if p.importMsgReqSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.importMsgReqMap) == pmapiImportBatchMaxItems {
p.importMessages(progress)
if progress.shouldStop() {
return
}
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) {
@ -176,7 +197,9 @@ func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox
return nil, errors.Wrap(err, "failed to parse message")
}
p.timeIt.start("encrypt", msg.ID)
body, err := p.encryptMessage(message, attachmentReaders)
p.timeIt.stop("encrypt", msg.ID)
if err != nil {
return nil, errors.Wrap(err, "failed to encrypt message")
}
@ -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) {
p.timeIt.start("parse", msg.ID)
defer p.timeIt.stop("parse", msg.ID)
// Old message parser is panicking in some cases.
// Instead of crashing we try to convert to regular error.
defer func() {
@ -254,26 +280,39 @@ func computeMessageFlags(labels []string) (flag int64) {
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() {
return
}
importMsgIDs := []string{}
importMsgRequests := []*pmapi.ImportMsgReq{}
for msgID, req := range p.importMsgReqMap {
for msgID, req := range importRequests {
importMsgIDs = append(importMsgIDs, msgID)
importMsgRequests = append(importMsgRequests, req)
}
log.WithField("msgIDs", importMsgIDs).WithField("size", p.importMsgReqSize).Debug("Importing messages")
results, err := p.importRequest(importMsgRequests)
log.WithField("msgIDs", importMsgIDs).Trace("Importing messages")
results, err := p.importRequest(importMsgIDs[0], importMsgRequests)
// In case the whole request failed, try to import every message one by one.
if err != nil || len(results) == 0 {
log.WithError(err).Warning("Importing messages failed, trying one by one")
for msgID, req := range p.importMsgReqMap {
importedID, err := p.importMessage(progress, req)
for msgID, req := range importRequests {
importedID, err := p.importMessage(msgID, progress, req)
progress.messageImported(msgID, importedID, err)
}
return
@ -285,20 +324,17 @@ func (p *PMAPIProvider) importMessages(progress *Progress) {
if result.Error != nil {
log.WithError(result.Error).WithField("msg", msgID).Warning("Importing message failed, trying alone")
req := importMsgRequests[index]
importedID, err := p.importMessage(progress, req)
importedID, err := p.importMessage(msgID, progress, req)
progress.messageImported(msgID, importedID, err)
} else {
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 {
results, err := p.importRequest([]*pmapi.ImportMsgReq{req})
results, err := p.importRequest(msgSourceID, []*pmapi.ImportMsgReq{req})
if err != nil {
return errors.Wrap(err, "failed to import messages")
}

View File

@ -18,6 +18,7 @@
package transfer
import (
"fmt"
"io"
"time"
@ -71,6 +72,11 @@ func (p *PMAPIProvider) tryReconnect() error {
func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*pmapi.Message, count int, err 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)
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) {
err = p.ensureConnection(func() error {
p.timeIt.start("download", msgID)
defer p.timeIt.stop("download", msgID)
message, err = p.client().GetMessage(msgID)
return err
})
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 {
p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID)
res, err = p.client().Import(req)
return err
})
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 {
p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID)
draft, err = p.client().CreateDraft(message, parent, action)
return err
})
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 {
// 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)
return err
})

View File

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

View File

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

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