Compare commits

...

32 Commits

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

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

Fixed
* Linux font issues - Fedora specific
* App response to the user pausing and canceling import or export
* Upgrade errors
2020-11-27 07:37:08 +01:00
f96cd167ef Merge branch 'release/golden-gate' into devel 2020-11-26 09:32:06 +01:00
d043cb9086 test: disable flaky expunge tests (followup GODT-881) 2020-11-23 14:41:47 +00:00
89 changed files with 1683 additions and 575 deletions

View File

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

View File

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

View File

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

View File

@ -10,8 +10,8 @@ TARGET_OS?=${GOOS}
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.5.2-git
IE_APP_VERSION?=1.2.1-git
BRIDGE_APP_VERSION?=1.5.4-git
IE_APP_VERSION?=1.2.3-git
APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns
@ -64,6 +64,12 @@ ifeq "${TARGET_CMD}" "Import-Export"
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
endif
ifdef QT_API
VENDOR_TARGET:=prepare-vendor update-qt-docs
else
VENDOR_TARGET=update-vendor
endif
build: ${TGZ_TARGET}
build-ie:
TARGET_CMD=Import-Export $(MAKE) build
@ -106,7 +112,7 @@ ifneq "${GOOS}" "${TARGET_OS}"
endif
endif
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET}
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
cp cmd/${TARGET_CMD}/main.go .
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
@ -123,7 +129,7 @@ icon_windows.syso: icon.rc logo.ico
## Rules for therecipe/qt
.PHONY: prepare-vendor update-vendor
.PHONY: prepare-vendor update-vendor update-qt-docs
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
# vendor folder will be deleted by gomod hence we cache the big repo
@ -148,6 +154,8 @@ prepare-vendor:
update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
${LINKCMD}
update-qt-docs:
go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API)
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated
@ -209,15 +217,18 @@ coverage: test
mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
lint: lint-golang lint-license
lint: lint-golang lint-license lint-changelog
lint-license:
./utils/missing_license.sh check
lint-changelog:
./utils/changelog_linter.sh
lint-golang:
which golangci-lint || $(MAKE) install-linter
golangci-lint run ./...

View File

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

5
go.mod
View File

@ -6,7 +6,6 @@ go 1.13
// They are in a separate require block to highlight this.
require (
github.com/docker/docker-credential-helpers v0.6.3
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
)
@ -18,7 +17,7 @@ require (
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-rfc5322 v0.2.1
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp/v2 v2.0.1
github.com/PuerkitoBio/goquery v1.5.1
@ -38,6 +37,7 @@ require (
github.com/emersion/go-mbox v1.0.2
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.14.0
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
github.com/fatih/color v1.9.0
@ -75,7 +75,6 @@ require (
replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474
github.com/emersion/go-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-20200818122824-ed5d25e28db8
)

9
go.sum
View File

@ -25,10 +25,8 @@ github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDE
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-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-rfc5322 v0.2.1 h1:J2PHusboDAYUfE+uBfoJnKZPbnVmzK1zXw6dQrgV8yE=
github.com/ProtonMail/go-rfc5322 v0.2.1/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
@ -94,6 +92,8 @@ github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1:k
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.14.0 h1:RYW203p+EcPjL8Z/ZpT9lZ6iOc8MG1MQzEx1UKEkXlA=
github.com/emersion/go-smtp v0.14.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
@ -310,6 +310,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

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 Tue Nov 24 08:56:01 AM CET 2020. DO NOT EDIT.
// Code generated by ./credits.sh at Fri Nov 27 09:23:06 CET 2020. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -15,18 +15,16 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Mon Nov 23 07:38:53 AM CET 2020'. DO NOT EDIT.
// Code generated by ./release-notes.sh at 'Wed Dec 9 05:55:04 PM CET 2020'. DO NOT EDIT.
package bridge
const ReleaseNotes = `Improved package creation logic
Refactor of sending functions to simplify code maintenance
Added tests for package creation
• For more detailed summary of the changes see https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
const ReleaseNotes = `Support read confirmations
Adding GPLv3 licence button to the GUI
Improved testing
`
const ReleaseFixedBugs = `Bridge crashes related to labels handling
GUI popup related to TLS connection error
An issue where a random session key is included in the data payload
• Error handling (including improved detection)
const ReleaseFixedBugs = `AppleMail crashes (timestamp related)
Encoding errors
Installation issues on linux
`

View File

@ -22,6 +22,7 @@ import (
"runtime"
"github.com/ProtonMail/proton-bridge/pkg/constants"
pkgSentry "github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/getsentry/sentry-go"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
@ -54,6 +55,7 @@ func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) e
err := sentry.Init(sentry.ClientOptions{
Dsn: constants.DSNSentry,
Release: constants.Revision,
BeforeSend: pkgSentry.EnhanceSentryEvent,
})
sentry.ConfigureScope(func(scope *sentry.Scope) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,6 +40,10 @@ import (
openpgperrors "golang.org/x/crypto/openpgp/errors"
)
var (
rfc822Birthday = time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC) //nolint[gochecknoglobals]
)
type doNotCacheError struct{ e error }
func (dnc *doNotCacheError) Error() string { return dnc.e.Error() }
@ -605,7 +609,7 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (
}
tmpBuf := &bytes.Buffer{}
mainHeader := message.GetHeader(m)
mainHeader := buildHeader(m)
if err = writeHeader(tmpBuf, mainHeader); err != nil {
return
}
@ -703,3 +707,23 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (
}
return structure, msgBody, err
}
func buildHeader(msg *pmapi.Message) textproto.MIMEHeader {
header := message.GetHeader(msg)
msgTime := time.Unix(msg.Time, 0)
// Apple Mail crashes fetching messages with date older than 1970.
// There is no point having message older than RFC itself, it's not possible.
d, err := msg.Header.Date()
if err != nil || d.Before(rfc822Birthday) || msgTime.Before(rfc822Birthday) {
if err != nil || d.IsZero() {
header.Set("X-Original-Date", msgTime.Format(time.RFC1123Z))
} else {
header.Set("X-Original-Date", d.Format(time.RFC1123Z))
}
header.Set("Date", rfc822Birthday.Format(time.RFC1123Z))
}
return header
}

View File

@ -46,6 +46,9 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
im.user.backend.setUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationUpdateMessage)
defer im.user.backend.unsetUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationUpdateMessage)
messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
if err != nil || len(messageIDs) == 0 {
return err

View File

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

View File

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

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 Tue Nov 24 08:56:01 AM CET 2020. DO NOT EDIT.
// Code generated by ./credits.sh at Fri Nov 27 09:23:06 CET 2020. DO NOT EDIT.
package importexport
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

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

View File

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

View File

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

View File

@ -44,7 +44,11 @@ type smtpUser struct {
backend *smtpBackend
user bridgeUser
storeUser storeUserProvider
username string
addressID string
from string
to []string
}
// newSMTPUser returns struct implementing go-smtp/session interface.
@ -53,8 +57,9 @@ func newSMTPUser(
eventListener listener.Listener,
smtpBackend *smtpBackend,
user bridgeUser,
username string,
addressID string,
) (goSMTPBackend.User, error) {
) (goSMTPBackend.Session, error) {
storeUser := user.GetStore()
if storeUser == nil {
return nil, errors.New("user database is not initialized")
@ -66,6 +71,7 @@ func newSMTPUser(
backend: smtpBackend,
user: user,
storeUser: storeUser,
username: username,
addressID: addressID,
}, nil
}
@ -145,6 +151,55 @@ func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey,
return su.client().GetPublicKeysForEmail(recipient)
}
// Discard currently processed message.
func (su *smtpUser) Reset() {
log.Trace("Resetting the session")
su.from = ""
su.to = []string{}
}
// Set return path for currently processed message.
func (su *smtpUser) Mail(from string, opts goSMTPBackend.MailOptions) error {
log.WithField("from", from).WithField("opts", opts).Trace("Setting mail from")
// REQUIRETLS and SMTPUTF8 have to be announced to be used by client.
// Bridge does not use those extensions so this should not happen.
if opts.RequireTLS {
return errors.New("REQUIRETLS extension is not supported")
}
if opts.UTF8 {
return errors.New("SMTPUTF8 extension is not supported")
}
if opts.Auth != nil && *opts.Auth != "" && *opts.Auth != su.username {
return errors.New("changing identity is not supported")
}
su.from = from
return nil
}
// Add recipient for currently processed message.
func (su *smtpUser) Rcpt(to string) error {
log.WithField("to", to).Trace("Adding recipient")
if to != "" {
su.to = append(su.to, to)
}
return nil
}
// Set currently processed message contents and send it.
func (su *smtpUser) Data(r io.Reader) error {
log.Trace("Sending the message")
if su.from == "" {
return errors.New("missing sender")
}
if len(su.to) == 0 {
return errors.New("missing recipient")
}
return su.Send(su.from, su.to, r)
}
// Send sends an email from the given address to the given addresses with the given body.
func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err error) { //nolint[funlen]
// Called from go-smtp in goroutines - we need to handle panics for each function.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,8 +18,6 @@
package store
import (
"time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -233,6 +231,7 @@ func (storeMailbox *Mailbox) RemoveDeleted() error {
return err
}
case pmapi.DraftLabel:
storeMailbox.log.WithField("ids", apiIDs).Warn("Deleting drafts")
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
return err
}
@ -280,6 +279,7 @@ func (storeMailbox *Mailbox) deleteFromTrashOrSpam(apiIDs []string) error {
}
}
if len(messageIDsToDelete) > 0 {
storeMailbox.log.WithField("ids", messageIDsToDelete).Warn("Deleting messages")
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
return err
}
@ -356,7 +356,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
}
isMarkedAsDeleted := deletedBucket.Get([]byte(msg.ID)) != nil
if seqErr == nil {
storeMailbox.store.imapUpdateMessage(
storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
btoi(uidb),
@ -390,7 +390,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
if err != nil {
return errors.Wrap(err, "cannot get sequence number from UID")
}
storeMailbox.store.imapUpdateMessage(
storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
uid,
@ -441,7 +441,7 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
}
if seqNumErr == nil {
storeMailbox.store.imapDeleteMessage(
storeMailbox.store.notifyDeleteMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
seqNum,
@ -459,7 +459,7 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
if err != nil {
return errors.Wrap(err, "cannot get counts for mailbox status update")
}
storeMailbox.store.imapMailboxStatus(
storeMailbox.store.notifyMailboxStatus(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
total,
@ -503,7 +503,7 @@ func (storeMailbox *Mailbox) txMarkMessagesAsDeleted(tx *bolt.Tx, apiIDs []strin
// In order to send flags in format
// S: * 2 FETCH (FLAGS (\Deleted \Seen))
update := storeMailbox.store.imapUpdateMessage(
storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
uid,
@ -511,14 +511,6 @@ func (storeMailbox *Mailbox) txMarkMessagesAsDeleted(tx *bolt.Tx, apiIDs []strin
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -253,6 +253,10 @@ func (m *Message) HasLabelID(labelID string) bool {
return false
}
func (m *Message) IsEncrypted() bool {
return strings.HasPrefix(m.Header.Get("Content-Type"), "multipart/encrypted") || m.IsBodyEncrypted()
}
func (m *Message) IsBodyEncrypted() bool {
trimmedBody := strings.TrimSpace(m.Body)
return strings.HasPrefix(trimmedBody, MessageHeader) &&

View File

@ -26,10 +26,15 @@ import (
log "github.com/sirupsen/logrus"
)
var (
skippedFunctions = []string{} //nolint[gochecknoglobals]
)
// ReportSentryCrash reports a sentry crash.
func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error) (err error) {
func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error) error {
SkipDuringUnwind()
if reportErr == nil {
return
return nil
}
tags := map[string]string{
@ -40,16 +45,71 @@ func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error)
"UserID": "",
}
var reportID string
sentry.WithScope(func(scope *sentry.Scope) {
SkipDuringUnwind()
scope.SetTags(tags)
sentry.CaptureException(reportErr)
eventID := sentry.CaptureException(reportErr)
if eventID != nil {
reportID = string(*eventID)
}
})
if !sentry.Flush(time.Second * 10) {
log.WithField("error", reportErr).Error("failed to report sentry error")
log.WithField("error", reportErr).Error("Failed to report sentry error")
return errors.New("failed to report sentry error")
}
log.WithField("error", reportErr).Warn("reported sentry error")
log.WithField("error", reportErr).WithField("id", reportID).Warn("Sentry error reported")
return nil
}
// SkipDuringUnwind removes caller from the traceback.
func SkipDuringUnwind() {
pcs := make([]uintptr, 2)
n := runtime.Callers(2, pcs)
if n == 0 {
return
}
frames := runtime.CallersFrames(pcs)
frame, _ := frames.Next()
if isFunctionFilteredOut(frame.Function) {
return
}
skippedFunctions = append(skippedFunctions, frame.Function)
}
// EnhanceSentryEvent swaps type with value and removes panic handlers from the stacktrace.
func EnhanceSentryEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
for idx, exception := range event.Exception {
exception.Type, exception.Value = exception.Value, exception.Type
if exception.Stacktrace != nil {
exception.Stacktrace.Frames = filterOutPanicHandlers(exception.Stacktrace.Frames)
}
event.Exception[idx] = exception
}
return event
}
func filterOutPanicHandlers(frames []sentry.Frame) []sentry.Frame {
newFrames := []sentry.Frame{}
for _, frame := range frames {
// Sentry splits runtime.Frame.Function into Module and Function.
function := frame.Module + "." + frame.Function
if !isFunctionFilteredOut(function) {
newFrames = append(newFrames, frame)
}
}
return newFrames
}
func isFunctionFilteredOut(function string) bool {
for _, skipFunction := range skippedFunctions {
if function == skipFunction {
return true
}
}
return false
}

66
pkg/sentry/report_test.go Normal file
View File

@ -0,0 +1,66 @@
// 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 sentry
import (
"testing"
r "github.com/stretchr/testify/require"
"github.com/getsentry/sentry-go"
)
func TestSkipDuringUnwind(t *testing.T) {
// More calls in one function adds it only once.
SkipDuringUnwind()
SkipDuringUnwind()
func() {
SkipDuringUnwind()
SkipDuringUnwind()
}()
wantSkippedFunctions := []string{
"github.com/ProtonMail/proton-bridge/pkg/sentry.TestSkipDuringUnwind",
"github.com/ProtonMail/proton-bridge/pkg/sentry.TestSkipDuringUnwind.func1",
}
r.Equal(t, wantSkippedFunctions, skippedFunctions)
}
func TestFilterOutPanicHandlers(t *testing.T) {
skippedFunctions = []string{
"github.com/ProtonMail/proton-bridge/pkg/config.(*PanicHandler).HandlePanic",
"github.com/ProtonMail/proton-bridge/pkg/config.HandlePanic",
"github.com/ProtonMail/proton-bridge/pkg/sentry.ReportSentryCrash",
"github.com/ProtonMail/proton-bridge/pkg/sentry.ReportSentryCrash.func1",
}
frames := []sentry.Frame{
{Module: "github.com/ProtonMail/proton-bridge/internal/cmd", Function: "main"},
{Module: "github.com/urfave/cli", Function: "(*App).Run"},
{Module: "github.com/ProtonMail/proton-bridge/internal/cmd", Function: "RegisterHandlePanic"},
{Module: "github.com/ProtonMail/pkg", Function: "HandlePanic"},
{Module: "main", Function: "run"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "(*PanicHandler).HandlePanic"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "HandlePanic"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/sentry", Function: "ReportSentryCrash"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/sentry", Function: "ReportSentryCrash.func1"},
}
gotFrames := filterOutPanicHandlers(frames)
r.Equal(t, frames[:5], gotFrames)
}

View File

@ -1,4 +1,3 @@
Bridge crashes related to labels handling
GUI popup related to TLS connection error
An issue where a random session key is included in the data payload
• Error handling (including improved detection)
AppleMail crashes (timestamp related)
Encoding errors
Installation issues on linux

View File

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

View File

@ -1,4 +1,3 @@
Improved package creation logic
Refactor of sending functions to simplify code maintenance
Added tests for package creation
• For more detailed summary of the changes see https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
Support read confirmations
Adding GPLv3 licence button to the GUI
Improved testing

View File

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

View File

@ -72,6 +72,7 @@ type TestContext struct {
transferLocalRootForExport string
transferRemoteIMAPServer *mocks.IMAPServer
transferProgress *transfer.Progress
transferSkipEncryptedMessages bool
// Store releated variables.
bddMessageIDsToAPIIDs map[string]string

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,83 @@
Feature: SMTP initiation
Scenario: Empty FROM
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "MAIL FROM:<>"
Then SMTP response is "OK"
Scenario: Send without FROM and TO
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "DATA"
Then SMTP response is "SMTP error: 502 5.5.1 Missing RCPT TO command."
Scenario: Reset is the same as without FROM and TO
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "MAIL FROM:<user@pm.me>"
Then SMTP response is "OK"
When SMTP client sends "RCPT TO:<user@pm.me>"
Then SMTP response is "OK"
When SMTP client sends "RSET"
Then SMTP response is "OK"
When SMTP client sends "DATA"
Then SMTP response is "SMTP error: 502 5.5.1 Missing RCPT TO command"
Scenario: Send without FROM
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "RCPT TO:<user@pm.me>"
Then SMTP response is "SMTP error: 502 5.5.1 Missing MAIL FROM command."
Scenario: Send without TO
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "MAIL FROM:<user@pm.me>"
Then SMTP response is "OK"
When SMTP client sends "DATA"
Then SMTP response is "SMTP error: 502 5.5.1 Missing RCPT TO command."
Scenario: Send with empty FROM
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "MAIL FROM:<>"
Then SMTP response is "OK"
When SMTP client sends "RCPT TO:<user@pm.me>"
Then SMTP response is "OK"
When SMTP client sends "DATA"
Then SMTP response is "OK"
When SMTP client sends "hello\r\n."
Then SMTP response is "SMTP error: 554 5.0.0 Error: transaction failed, blame it on the weather: missing sender"
Scenario: Send with empty TO
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "MAIL FROM:<user@pm.me>"
Then SMTP response is "OK"
When SMTP client sends "RCPT TO:<>"
Then SMTP response is "OK"
When SMTP client sends "DATA"
Then SMTP response is "OK"
When SMTP client sends "hello\r\n."
Then SMTP response is "SMTP error: 554 5.0.0 Error: transaction failed, blame it on the weather: missing recipient"
Scenario: Allow BODY parameter of MAIL FROM command
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "MAIL FROM:<user@pm.me> BODY=7BIT"
Then SMTP response is "OK"
Scenario: Allow AUTH parameter of MAIL FROM command
Given there is connected user "user"
When SMTP client authenticates "user"
Then SMTP response is "OK"
When SMTP client sends "MAIL FROM:<user@pm.me> AUTH=<>"
Then SMTP response is "OK"

View File

@ -38,4 +38,4 @@ Feature: SMTP wrong messages
"""
Then SMTP response is "SMTP error: 554 Error: transaction failed, blame it on the weather: failed to create new parser: unexpected EOF"
Then SMTP response is "SMTP error: 554 5.0.0 Error: transaction failed, blame it on the weather: failed to create new parser: unexpected EOF"

View File

@ -0,0 +1,183 @@
Feature: Import from EML files
Background:
Given there is connected user "user"
And there is EML file "Inbox/clear.eml"
"""
Subject: clear
From: Bridge Test <bridgetest@pm.test>
To: Internal Bridge <test@protonmail.com>
secret
"""
And there is EML file "Inbox/encrypted.eml"
"""
Subject: encrypted
From: Bridge Test <bridgetest@pm.test>
To: Internal Bridge <test@protonmail.com>
-----BEGIN PGP MESSAGE-----
hQEMA7hGUUsYs0fEAQgA10NwJSNTLm3vpxVtoYBaA9AjFI5H4hKuV3/f2NHbWb2s
k5enK3tEIOYdFdrO+NLrV6weHq3Dgu4er3URTQ62tFwjSJyeXxmY0d9J+JdxibJg
wqZubuSHYzQHpFqJgoUUWEVEsv0Ao8yQo8BE9iybCKoZf6KojROUuE748oxlxJnV
m1XuaVIzgw4xN0GUA5sLLuWeL94b2dZe5SDDQE5POzDgueZ7faefX8U1pGErCRJ0
sO6FSw3SF4NpvrxVESWgCmsG5pcuxE2JqB0UoHnNDcqsW8w1Q+GabAPo6UqHhgIg
56MRCWeou2djHIIj9TMUIVFzSG/HvTYQWVS+i4Z7AdJJAXr53GgbZQznO80Qxwcb
FFdlwOXHuaXqhqCb338jlQWnbcnUsuJWxBAxkHrlP/nluFqPdIBOglC38kdYSBed
3YwuEB9sXV/fcw==
=B05V
-----END PGP MESSAGE-----
"""
And there is EML file "Inbox/encrypted-mime.eml"
"""
Subject: encrypted mime
From: Bridge Test <bridgetest@pm.test>
To: Internal Bridge <test@protonmail.com>
MIME-Version: 1.0
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="WLjzd46aUAiOcuNXjWTJItBZonI56MuAk"
--WLjzd46aUAiOcuNXjWTJItBZonI56MuAk
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
--WLjzd46aUAiOcuNXjWTJItBZonI56MuAk
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
hQEMA1ppSfinU0f4AQf9HDkojTV3SspsnhaB0HAsKIrUd+AAdSm49ndnJyjYb210
GFIDE/TqcXmoeOcaJIRWaEOZzdcnixplJHjwp5dvDyCaYQSqYxUQ5Z/JfKbtsDyV
HbQzAh833SBCFlNNTnmF/Onu7yRNje1k8U36bY1VUX1QlerT9HDm2QTMRheuPDUR
H9OvGkuBXRpWRSPyXlPONPQOZTbUxvkuMGgDY0N2wt6kKQsrtduQNC157EJOErq0
Zlhu9CnAyezDupMkSoikR1uyxo7GhyXNxi70Ol3tN7E2fnzeBCjUgmliYTABOGSH
nuPpTNk3/YoLEHXK18E/qR3vJTTl6AFIbOcfRCqpQIUBDAOjbxn1yC74AQEH/0kB
CiNDwPepRxwzv3EZT7V0YPuTCD18m9BZ4W5lVEvMNP7HJnCILJT8QJhLQ+AVBUuw
jhJqxAahssOGQ5BVxnWj4qwM+WzBOplH9Zt9bKTie8IdAJsl5GysL19jc4fjnvsK
weBQiR3Y+lEGEBrCajVrUkrXRHyA0fmel8aPfhiHxbh+jRtY8BWdBeX3gIfjwVKf
mMuTmHQ6ERv9CGpIy7mxRF67EIaVRhQzjNNnRlCIqgZHOpS72SKc6DtyCiR+ECjq
UAKNwOjTNZEzAjczyIB5Hkkw1trtVOZEqdacy0CM/SJxjRA8HQ7/0pjtqjOvcpZU
IB6z7IbZLH7krqJY4ZHS6gFvH3B7YOksaQsQL0x4GsdYY4mGUj/18Dzzw2YscUjs
HCOuN9zwAxEIztSQFFZ8vShbpk73fu80X5qRoCQ9708+sKdO92oDY9oZBQkcUl1T
qhpSdApN9mJl2n+uHfSDy63YynhT/bMMrh0AfZjB4ssX9jNkH2knS/FVFUjFUHVh
6boXr0q9xdxt64onx8BrpWOBCqqXjRWUR2n/+y+zw+YgjqUWjpVmsQoF7wQQ3xo6
Yb8y2WguTG9K6m9rS96dOtkXWJgZOVYZ5zlRqdbGZzlfei1890QfnRsNJQhhwkLq
CJV5bhy6AGZxk9JK/RW33g//i2GDfUx4HptRPEgGWu3ZdQskKwyZB7dc6NMtT2as
tOP6z/wgLIPVlLJEY0jXHkmbGf7Oj9JpBSCQBz57rmZunsTgy/jDIuL6mzeuVdYN
lVHqVao23aTZRPaCmwYqWW254oCKaeE7X8nRaQF+9L2nK4YbUf0+KbDGhjnQy8Qg
K1cQt51NcWsM28jNV6Puww7MS+K0NaMjr1fTHdomfHI27C0Dr1e85BWkDnesLqtw
2s5S/8KdYMdBLuzyfT4UQkYTmtxibRXQR9+TxDmNQ/luMuFTCowgGfebAMOCrwU7
NxrgSyuTmAC1Je1glSMMQghHwBCUB2BUCn/vFlMwHdl1waKrUpRaKQRI3iPhMjMw
91Fsv5cUc6uD2pO7vb+vOm2O7+i08KtBpttjk+ANDJjxiGT0V/omlh40T80vN0h6
yk8ZNTq8MqqvLMyH2wKqmmEjll73AWkHATLawRD3ckmlEF+ywc6J91CAYXokWuHc
N7CBL3vRhEJppZ3rmKNw3ani+ThQLTqnGxzxuB+P5IBO6RGXvjYfiUC3Nb0o1Q6X
+QD5BZlvVkklG4bwRdcn87wSlarA8T/nqlZ388ajNaE1Y2+zyJnJyOUEk3nLcgI8
ovaVF/G3PG4yhPR+oOgE7IdWwp+WFa15OF2iLn8ByQa3V8fsWczXHu/iXLyr0KKl
MJCR4bsCv2hcOFTlYSRMyBs+A9gXA9pT+ljv0g7/Z9BuFSmr6pRzgK/guk6WzoTt
m+TxDn1hEovo62KkhAyMtD1hbYO/5GDB6X8tI0YM0kRk8E+H8fuxl43uUE+y9B0X
7Qmkf1Oym9x23S+372MiEa/avAWZTtHhhii37lWkKU+pkx+aiMrfJyozafx6cAaQ
Rxx5uv+8lXEZy4qNEXop7yKDz2agSd6XdZziSIO69BF3x6DMKZdBJtyc5V2RqibU
t80ziVK0IStJmNUPZ1DSMXiwN3yzkQ/bm9RH3x3PPvaVNjISHdl85wlDFc8FM2m0
Q0RM40lj5XAEs1O8iBk5m9yCNMSKQLq5vOhmbygK3ILp4dBoYr6EGZjz+Nq4M+ws
n/dzdR62oCVuKYvVyJVUkmt4DGTo7Pi9ngjAdmLu/RLL8M0/MG9wbu2adT7c2ypj
HM3lUqm+KEf9CdpJBVj0RH5BDWKwDpWx6g6np+GoXsj9nkXYv5qxzVNwgpjTRHwH
xJE+1nFStBtiWunP6eqd8Fl99/jATgVU9ytp+Q+nnZPZn+KHCZEl3CF/TBKsNl7S
QUwdepNF80MDYFi2r685SqM6fvefur0sqyeDwsBOM4GBU88FH9GnWJhQqKVEmQH2
PV/UzkCPpj0ngkQiQjGMQjOKmI6npljOWbIw7LrhggOnfFnP2iTO0B4aAx1h6Ppi
3+jkrdJEuxB89f8P/W8ChtOw7s53YTwYtxmZ+/x0e1G4Nh8pPcFRFF2t/UHEav5v
s3CyH7reAIXDclHH46wbrczvcf6FzS+o8ypIRFAapamUhPqpksuIvyoUeQv8WW/Q
m2tFOPp9wJp/+GAEbuZTyTd/o7Cms3Zl0EOQB9tgqWyqWhasPd40/SCdeXzqpEMS
5Io0tE0ohY9DzN96kn2+07FUSqOYInup3+EXUhCGF8K/i1dny6/o7ZxDjW5xsTdb
AZxd0UEdhvJtvtKhckLhICzImeLGrCUz/zuJBvTR08ir8Rm8kkAmHBn9/jf8+42J
X3TSTes7+k+DtZP6VL6RKhTAzFIEWLQZ+38nzGPfM0BUKf0sGW3wlWFQREU9k9QX
S/idPNOqdNHz/l0eUwf3/bjfAB6OqitPWYH23d6zMkMEgwx/gJmboOfiYu9FKvXJ
tvRgOHb6Rww8fUQhlDOhVupo0DFTIghdeXjeVn/CxIUO67Ns+PI5IB+/sw1KxTIp
kZjjft/l3+mSnyJVyqvzKyfA1WhaXLXJfcJyeGt/Y45RiYnkbSdcbuNhngn+NpZZ
SAcS4vyUqkDQ6RvWU+fww8EYxptNALt9hnc1wD+e8b3Gz2citRrLrc4AhiZwafp8
gj4HtKBXFz3oX2vhHgubTLuiEKhGc2dYXL9Z2PlXOWZhauTO00iVYfbWPsdTRSvi
QmKIP9QVzDq6StfHxs2x47yxrHrEtCjsHjDz3d+r+p6i6O212EHlCQewaPfAieBp
lw01cJB1KwyeoYgTczQkz6hhM+fj1RBMNDqxTHBVb3GGNh1nxu+4UR+tgQG/V/Ot
M/1NE8+yeRktzukDX1toXfCFXvRL3ijriHliaivWww==
=4M3l
-----END PGP MESSAGE-----
--WLjzd46aUAiOcuNXjWTJItBZonI56MuAk--
"""
And there is EML file "Inbox/signed.eml"
"""
Subject: signed
From: Bridge Test <bridgetest@pm.test>
To: Internal Bridge <test@protonmail.com>
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
secret
-----BEGIN PGP SIGNATURE-----
iQEzBAEBCAAdFiEENaE2ZPemenI4pZah/SJcGo7SJWIFAl+cCAgACgkQ/SJcGo7S
JWKsOQf/YakNXkMNjZIu8Hf1WflxtiDXVzTugOicC05k5W64oIqSHt0xNaFKE37k
//3eDMWbHvqHKFVdg7qcLsVPeVBaW3bdZUiexGM24OiGgyEitufnHQLOtEDTound
JyH5nUeHpvpBKIIOJZNBDM0HsRYnwKwrOWk3N2VRwog4J8J3cmJ/f9bPWNI/0OPT
qmtVGRVg6Ge83nZn51Vof//jFzkO4wGYCsE0aF0Ywc7nISZuyKQzmu/qgmwzDG50
PjpvIQ/ygisRPNdRlylXEqyoIDCQ+v0AnxhhwX/5dbt6xMuMMOxBrFSC94Zce1Vj
x2ssXlT4ONPnkI/YWwhtQPLU628IMg==
=GiS3
-----END PGP SIGNATURE-----
"""
And there is EML file "Inbox/encrypted-signed.eml"
"""
Subject: encrypted and signed
From: Bridge Test <bridgetest@pm.test>
To: Internal Bridge <test@protonmail.com>
-----BEGIN PGP MESSAGE-----
hQEMA7hGUUsYs0fEAQf/dppHciWIf+o4l0gEfHeyHV/HVhG4es0aVQYrwFQlSWVx
estMuyLBSMfrsQXLago7Q9ZNo/XnKszzprCXxxYH52hAg64oAsjKB3jgRmVizs8b
8lj0BRf003wUluS/0msV9SiEZBGeL8jGq6Te9vaM8OHHhIVzVjGnRdTSC0jBE6cS
vy8IBHXYe0LfdZiPojPDPGQdSej+H3uu7eZGBvVHTDeQLPDel4k7Ykdr0qlNXs6O
5XpM5YG4w+t0aG+YROPH+BUj8PpPojQ/lrv/yFISTRbHlEd8N50w8BNTnBet+9Vm
oPcyvN+RQxBlvRuPpDjUmREvmtObKZV6+m6gocemx9LAzQEeVLcpjO/hJhl8gX72
MNz3McU7aXf5sSoOPdHDNx8T2NON/2bwG5FE+PRMuVywTKhCB7o8VAsJpGMQ8xRM
5WCNhow0AI7kni8yZA+GbvspnJWfit9tCTR5MIFHCSH9J3kJJnWkxQSN04GGpBcd
n43GWn7O7ufA4lMMZiGXMdi/J1iV9waAsIfMPk29BMq6xK0/jJYdHqQS+vNsSnF5
xL/Ir4RYq4SFFA06A/E7HpXr2ruZhBQCkzaIIdrVJR/Lp2VLJIVulTBQK8y2AFtj
JeeKS0kIuC/7UPF2O624kwNr8dmIhDJYusFs6ZeED/nAKwDO/vP2CSwVC3sUjn3N
u+sWqQUTxSmjhRVf9b0+VyTh0mXCovJQXomL6Zz6lxXuJqqzELIOfCxYD1z9GwTG
cT08Aa2eEpf3agdLCTxvjO3iq9FksMHvIN+LSCQ6Pw+aTByjrk0oMmvGbANAogTk
yrplG/iRVlmq0p/Cfl5UEjKqT/nt5j9zbpeuYXmhjiBT9SBE07oUVLY1VT7ihcY=
=qYnL
-----END PGP MESSAGE-----
"""
Scenario: Import encrypted
Given there is skip encrypted messages set to "false"
When user "user" imports local files
Then progress result is "OK"
And transfer failed for 0 messages
And transfer exported 5 messages
And transfer imported 5 messages
And transfer skipped 0 messages
And API mailbox "INBOX" for "user" has messages
| from | to | subject |
| bridgetest@pm.test | test@protonmail.com | clear |
| bridgetest@pm.test | test@protonmail.com | encrypted |
| bridgetest@pm.test | test@protonmail.com | encrypted mime |
| bridgetest@pm.test | test@protonmail.com | signed |
| bridgetest@pm.test | test@protonmail.com | encrypted and signed |
Scenario: Skip encrypted
Given there is skip encrypted messages set to "true"
When user "user" imports local files
Then progress result is "OK"
And transfer failed for 0 messages
And transfer exported 5 messages
And transfer imported 2 messages
And transfer skipped 3 messages
And API mailbox "INBOX" for "user" has messages
| from | to | subject |
| bridgetest@pm.test | test@protonmail.com | clear |
| bridgetest@pm.test | test@protonmail.com | signed |

View File

@ -32,6 +32,8 @@ func IMAPChecksFeatureContext(s *godog.Suite) {
s.Step(`^IMAP response to "([^"]*)" contains "([^"]*)"$`, imapResponseNamedContains)
s.Step(`^IMAP response has (\d+) message(?:s)?$`, imapResponseHasNumberOfMessages)
s.Step(`^IMAP response to "([^"]*)" has (\d+) message(?:s)?$`, imapResponseNamedHasNumberOfMessages)
s.Step(`^IMAP response does not contain "([^"]*)"$`, imapResponseDoesNotContain)
s.Step(`^IMAP response to "([^"]*)" does not contain "([^"]*)"$`, imapResponseNamedDoesNotContain)
s.Step(`^IMAP client receives update marking message seq "([^"]*)" as read within (\d+) seconds$`, imapClientReceivesUpdateMarkingMessageSeqAsReadWithin)
s.Step(`^IMAP client "([^"]*)" receives update marking message seq "([^"]*)" as read within (\d+) seconds$`, imapClientNamedReceivesUpdateMarkingMessageSeqAsReadWithin)
s.Step(`^IMAP client receives update marking message seq "([^"]*)" as unread within (\d+) seconds$`, imapClientReceivesUpdateMarkingMessageSeqAsUnreadWithin)
@ -73,6 +75,16 @@ func imapResponseNamedHasNumberOfMessages(clientID string, expectedCount int) er
return ctx.GetTestingError()
}
func imapResponseDoesNotContain(notExpectedResponse string) error {
return imapResponseNamedDoesNotContain("imap", notExpectedResponse)
}
func imapResponseNamedDoesNotContain(clientID, notExpectedResponse string) error {
res := ctx.GetIMAPLastResponse(clientID)
res.AssertNotSections(notExpectedResponse)
return ctx.GetTestingError()
}
func imapClientReceivesUpdateMarkingMessageSeqAsReadWithin(messageSeq string, seconds int) error {
return imapClientNamedReceivesUpdateMarkingMessageSeqAsReadWithin("imap", messageSeq, seconds)
}

View File

@ -160,6 +160,16 @@ func (ir *IMAPResponse) AssertSections(wantRegexps ...string) *IMAPResponse {
return ir
}
// AssertNotSections is similar to AssertSections but is the opposite.
// It means it just tries to find all "regexps" in the response.
func (ir *IMAPResponse) AssertNotSections(unwantedRegexps ...string) *IMAPResponse {
ir.wait()
for _, unwantedRegexp := range unwantedRegexps {
a.Error(ir.t, ir.hasSectionRegexp(unwantedRegexp), "regexp %v found\nSections: %v", unwantedRegexp, ir.sections)
}
return ir
}
// WaitForSections is the same as AssertSections but waits for `timeout` before giving up.
func (ir *IMAPResponse) WaitForSections(timeout time.Duration, wantRegexps ...string) {
a.Eventually(ir.t, func() bool {

View File

@ -33,10 +33,10 @@ func SMTPActionsAuthFeatureContext(s *godog.Suite) {
s.Step(`^SMTP client authenticates with username "([^"]*)" and password "([^"]*)"$`, smtpClientAuthenticatesWithUsernameAndPassword)
s.Step(`^SMTP client logs out$`, smtpClientLogsOut)
s.Step(`^SMTP client sends message$`, smtpClientSendsMessage)
s.Step(`^SMTP client sends EHLO$`, smtpClientSendsEHLO)
s.Step(`^SMTP client "([^"]*)" sends message$`, smtpClientNamedSendsMessage)
s.Step(`^SMTP client sends message with bcc "([^"]*)"$`, smtpClientSendsMessageWithBCC)
s.Step(`^SMTP client "([^"]*)" sends message with bcc "([^"]*)"$`, smtpClientNamedSendsMessageWithBCC)
s.Step(`^SMTP client sends "([^"]*)"$`, smtpClientSendsCommand)
}
func smtpClientAuthenticates(bddUserID string) error {
@ -93,12 +93,6 @@ func smtpClientSendsMessage(message *gherkin.DocString) error {
return smtpClientNamedSendsMessage("smtp", message)
}
func smtpClientSendsEHLO() error {
res := ctx.GetSMTPClient("smtp").SendCommands("EHLO ateist.test")
ctx.SetSMTPLastResponse("smtp", res)
return nil
}
func smtpClientNamedSendsMessage(clientID string, message *gherkin.DocString) error {
return smtpClientNamedSendsMessageWithBCC(clientID, "", message)
}
@ -112,3 +106,11 @@ func smtpClientNamedSendsMessageWithBCC(clientID, bcc string, message *gherkin.D
ctx.SetSMTPLastResponse(clientID, res)
return nil
}
func smtpClientSendsCommand(command string) error {
command = strings.ReplaceAll(command, "\\r", "\r")
command = strings.ReplaceAll(command, "\\n", "\n")
res := ctx.GetSMTPClient("smtp").SendCommands(command)
ctx.SetSMTPLastResponse("smtp", res)
return nil
}

View File

@ -20,6 +20,7 @@ package tests
import (
"fmt"
"net/mail"
"net/textproto"
"strconv"
"strings"
"time"
@ -97,6 +98,7 @@ func thereAreMessagesInMailboxesForAddressOfUser(mailboxNames, bddAddressID, bdd
LabelIDs: labelIDs,
AddressID: account.AddressID(),
}
header := make(textproto.MIMEHeader)
if message.HasLabelID(pmapi.SentLabel) {
message.Flags |= pmapi.FlagSent
@ -143,12 +145,14 @@ func thereAreMessagesInMailboxesForAddressOfUser(mailboxNames, bddAddressID, bdd
return internalError(err, "parsing time")
}
message.Time = date.Unix()
header.Set("Date", date.Format(time.RFC1123Z))
case "deleted":
hasDeletedFlag = cell.Value == "true"
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
}
message.Header = mail.Header(header)
lastMessageID, err := ctx.GetPMAPIController().AddUserMessage(account.Username(), message)
if err != nil {
return internalError(err, "adding message")

View File

@ -143,6 +143,7 @@ func doTransfer(bddUserID, bddAddressID string, rules *gherkin.DataTable, getTra
if err := setRules(transferrer, rules); err != nil {
return internalError(err, "failed to set rules")
}
transferrer.SetSkipEncryptedMessages(ctx.GetTransferSkipEncryptedMessages())
progress := transferrer.Start()
ctx.SetTransferProgress(progress)
return nil

View File

@ -40,6 +40,7 @@ func TransferChecksFeatureContext(s *godog.Suite) {
s.Step(`^progress result is "([^"]*)"$`, progressFinishedWith)
s.Step(`^transfer exported (\d+) messages$`, transferExportedNumberOfMessages)
s.Step(`^transfer imported (\d+) messages$`, transferImportedNumberOfMessages)
s.Step(`^transfer skipped (\d+) messages$`, transferSkippedNumberOfMessages)
s.Step(`^transfer failed for (\d+) messages$`, transferFailedForNumberOfMessages)
s.Step(`^transfer exported messages$`, transferExportedMessages)
s.Step(`^exported messages match the original ones$`, exportedMessagesMatchTheOriginalOnes)
@ -77,6 +78,13 @@ func transferImportedNumberOfMessages(wantCount int) error {
return ctx.GetTestingError()
}
func transferSkippedNumberOfMessages(wantCount int) error {
progress := ctx.GetTransferProgress()
counts := progress.GetCounts()
a.Equal(ctx.GetTestingT(), uint(wantCount), counts.Skipped)
return ctx.GetTestingError()
}
func transferFailedForNumberOfMessages(wantCount int) error {
progress := ctx.GetTransferProgress()
failedMessages := progress.GetFailedMessages()

View File

@ -41,6 +41,7 @@ func TransferSetupFeatureContext(s *godog.Suite) {
s.Step(`^there are IMAP mailboxes$`, thereAreIMAPMailboxes)
s.Step(`^there are IMAP messages$`, thereAreIMAPMessages)
s.Step(`^there is IMAP message in mailbox "([^"]*)" with seq (\d+), uid (\d+), time "([^"]*)" and subject "([^"]*)"$`, thereIsIMAPMessage)
s.Step(`^there is skip encrypted messages set to "([^"]*)"$`, thereIsSkipEncryptedMessagesSetTo)
}
func thereAreEMLFiles(messages *gherkin.DataTable) error {
@ -259,3 +260,15 @@ func createFile(fileName, body string) error {
_, err = f.WriteString(body)
return internalError(err, "failed to write to file")
}
func thereIsSkipEncryptedMessagesSetTo(value string) error {
switch value {
case "true":
ctx.SetTransferSkipEncryptedMessages(true)
case "false":
ctx.SetTransferSkipEncryptedMessages(false)
default:
return fmt.Errorf("expected either true or false, was %v", value)
}
return nil
}

View File

@ -6,6 +6,6 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Added
### Changed
### Removed
### Fixed

140
utils/changelog_linter.sh Executable file
View File

@ -0,0 +1,140 @@
#!/bin/bash
# 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/>.
CHANGELOG_FILE="`dirname $0`/../Changelog.md"
ERROR_COUNT_FILE="`mktemp`"
echo "0">$ERROR_COUNT_FILE
##########################
# -- Helper functions -- #
##########################
# err print out a given error ($2) and line where it hapens ($1),
# also it increases count of errors.
err () {
echo "CHANGELOG-LINTER: $2 on the following line:"
echo "$1"
echo $((`cat $ERROR_COUNT_FILE` + 1)) > $ERROR_COUNT_FILE
}
# trim removes extra whitespaces.
trim () {
echo "$1"|sed 's/[ \t]*$//g;s/^[ \t]//g'
}
# paragraph_continues checks that given line ends with sentence ending symbol like [.!?:].
is_ended() {
ENDING="${1: -1}"
[ "$ENDING" == "." ] || [ "$ENDING" == "!" ] || [ "$ENDING" == "?" ] || [ "$ENDING" == ":" ]
}
# paragraph_continues checks that given line starts with a small letter.
# (so it's not a start of a new sentence or block.)
paragraph_continues() {
LINE_START="${1:0:1}"
echo "$LINE_START" | grep -q "[a-z]"
}
##########################
# -- Linter functions -- #
##########################
# check_lists checks a format of lists.
# - Starting with " * " (minus lists is not allowed).
# - Containing whole sentence with first capital letter and dot at the end.
check_lists () {
if [ "${1:0:2}" == "- " ]; then
err "$1" '"-" lists is not allowed, use "*" instead'
fi
if [ "${1:0:2}" != "* " ]; then # It's not a list. Skipping...
return
fi
if ! is_ended "$1" && ! paragraph_continues "$2"; then
err "$1" "List should contain a full sentence ending with one of the [!?.:] symbols"
fi
if ! (echo "${1:0:3}" | grep -q "[A-Z\#\`\"]"); then
err "$1" "List should contain a full sentence starting with a capital letter"
fi
}
# check_change_types checks a format change type headings.
# - See https://keepachangelog.com/en/1.0.0/ for allowed formats.
check_change_types () {
if [ "${1:0:4}" != "### " ]; then # It's not a type heading. Skipping...
return
fi
case "$1" in
"### Added"|"### Changed"|"### Deprecated"|"### Removed"|"### Fixed"|"### Security") ;; # Standard keepachangelog.com compliant types.
"### Release notes"|"### Fixed bugs") ;; # Bridge aditional in app release notes types.
"### Guiding Principles"|"### Types of changes") ;; # Ignoring guide at the end of the changelog.
*) err "$1" "Change type must be one of the Added, Changed, Deprecated, Removed, Fixed, Hoftix"
esac
}
# check_application_name checks if a application name is defined on each record.
check_application_name () {
if [ "${1:0:4}" != "## [" ]; then # It's not a version heading. Skipping...
return
fi
case "`echo $1|cut -d"[" -f2|cut -d" " -f1`" in
"IE"|"Bridge") ;;
*) err "$1" "Either \"IE\" or \"Bridge\" application name should be inside the version header"
esac
}
# ignored_line determines lines which will not be linted.
ignored_line () {
[ "$1" == "" ] # Ignoring empty lines.
}
######################
# -- Main routine -- #
######################
LINE_BEFORE="" # Storing the line before for some multiline operations.
cat $CHANGELOG_FILE|while read L; do
LINE=`trim "$L"`
if ignored_line "$LINE"; then
continue
fi
check_lists "$LINE_BEFORE" "$LINE"
check_change_types "$LINE"
check_application_name "$LINE"
LINE_BEFORE="$LINE"
done
ERROR_COUNT=`cat $ERROR_COUNT_FILE`
rm $ERROR_COUNT_FILE
echo "CHANGELOG-LINTER found $ERROR_COUNT problems."|sed "s/found 0 problems/passed successfully ;)/"
exit $ERROR_COUNT